Web surfaces
Three sibling Next.js 16 (App Router) apps deployed to AWS Amplify on their
own drape.to subdomains. The repos configure output: "export", but Amplify
actually runs them on the WEB_COMPUTE platform (its Next.js server runtime) —
see Infrastructure for the verified hosting facts. All three
are frontends only — data comes from the Postgres backend
at api.drape.to. The brand and admin portals share Drape's
design tokens; the marketing site has its own extended
editorial palette.
Marketing — www.drape.to
Drape-Marketing is the public acquisition site — no auth, fully static. Its route
map:
| Route | Purpose |
|---|---|
/ | Landing: hero → product demo → trust → shopper value → download or waitlist (gated on NEXT_PUBLIC_DOWNLOADS_ENABLED) |
/download | Store links or waitlist form depending on the same flag |
/for-brands | Brand recruitment — features, plans, how-it-works, FAQ; CTAs deep-link to brand.drape.to/apply |
/join | Referral landing — reads ?ref=CODE, stores it in localStorage under drape_referral_code |
`/product/[id]` | Client-side product share page — fetches `GET /api/products/{id}`, renders an "Open in Drape" deep link (drape://product/{id}) |
It calls only two backend endpoints (`GET /api/products/{id}` and
POST /api/marketing/waitlist) and never sends an auth header. The verbatim
product and plan copy it holds is documented under
For brands.
Brand portal — brand.drape.to and {slug}.drape.to
Drape-brand is the desktop counterpart to the mobile brand portal, and it is
multi-tenant by subdomain. A proxy.ts (Next.js 16's renamed middleware) reads
the host and sets a tenant slug header:
- Reserved host (
brand.drape.to) serves the marketing landing,/applyonboarding wizard,/signup,/login, and/verify-email. - Tenant host (
{slug}.drape.to, e.g.prk.drape.to) serves the store's admin dashboard. Login on the reserved host performs a cross-subdomain handoff — it redirects to{slug}.drape.to/auth/handoff#token=…&user=…, which validates portal access and slug match, then lands on/dashboard.
The {slug}.drape.to tenant model is built in code and wired at DNS:
Route 53 has a *.drape.to CNAME to the brand CloudFront hostname, and the
Amplify domain association for Drape-brand includes brand + *. Amplify
still reports verified=false on those names while domainStatus is
AVAILABLE — a metadata issue, not “wildcard unwired.” See
Infrastructure and the
AWS system map.
The authenticated dashboard is large and RBAC-gated (the sidebar shows only permitted sections): dashboard, products (+ new/edit, CSV import/export, QR), orders, analytics (charts), content + comment moderation, go-live (streams), engagement (drops / waitlists / UGC / price-drops), retail (in-store checkout + QR scanner), locations (+ door QR + inventory), team (roles/invites), settings, billing (Stripe Connect + subscription), integrations (webhooks), widget, and agreement.
Auth & access: brandFetch() attaches the bearer token, pre-checks
isTokenExpired() (30s skew) and hard-redirects to /login on expiry or any 401.
The token lives in localStorage under drape_brand_token. The access gate is
is_admin or (is_store_admin + store_id) or (store_id + role_id).
A TenantProvider fetches `GET /api/tenant/{slug}` and applies the store's
brand_colors as CSS custom properties at runtime.
Public plans: Freemium ($0 / 10%, 25-product cap, analytics + Connect),
Basic ($19.99 / 8%), Premium ($29.99 / 6%). Live prices come from
admin brand_plan_catalog via GET /api/marketing/brand-plans; client
fallbacks are in drape-brand-v1/lib/billingPlans.ts and
drape-marketing/lib/brandMarketing.ts. Grandfathered ($0 / 5%) is
admin-only with a required end date and is never listed for self-serve checkout.
Admin — admin.drape.to
Drape-admin-v1 is the internal ops dashboard that replaced the old in-app
app/admin/* screens. Public /login, then an authenticated shell grouped as
Dashboard · Catalog (stores / products / featured / drops) · Onboarding
(submissions / brand-waitlist) · Community (moderation / UGC / referrals) · People
(users / inbox) · Platform (integrations / analytics / audit-logs / ops).
Notable: a full TipTap rich-email composer powers outbound campaigns and an
inbound mailbox (/inbox), and a shared bulk-action pattern (select rows →
confirm → bulk email / status change) spans stores, products, users, waitlist, and
submissions.
Auth: adminFetch() mirrors the brand client (bearer token, client-side
expiry pre-check, redirect on 401); token in localStorage under
drape_admin_token; every admin call requires is_admin.
The admin JWT is browser-only localStorage, accepted as "internal ops." The
admin README is explicit that the frontend is not the authorization boundary —
every write is enforced is_admin server-side. CORS must allow admin.drape.to
and *.drape.to. Do not treat the client-side gate as security.
Shared conventions
- Stack: Next.js 16.2.x, React 19.2, Tailwind v4, TypeScript 5, Inter font, ESLint flat config.
- Auth: JWT in
localStoragewith a client-sideisTokenExpired()guard (30s skew) that redirects to/loginon 401/expiry. Keys differ per app (drape_brand_tokenvsdrape_admin_token). - Deploy: AWS Amplify in us-east-1 on the WEB_COMPUTE platform (
main= prod). Git connections aredrape-ai-llc/*(org), notKCB1099/*. Verified app IDs and domains are on the Infrastructure page. - Backend:
NEXT_PUBLIC_BACKEND_URL→api.drape.to(devdev-api.drape.to).staging.drape.tois not in the live Route 53 zone map.