Skip to main content

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.

Shared conventions

Next.js 16 · React 19 · Tailwind v4
Amplify WEB_COMPUTE · JWT in localStorage
client-side isTokenExpired guard

Drape-Marketing
www.drape.to

Drape-brand
brand.drape.to + {slug}.drape.to

Drape-admin-v1
admin.drape.to

Marketing — www.drape.to

Drape-Marketing is the public acquisition site — no auth, fully static. Its route map:

RoutePurpose
/Landing: hero → product demo → trust → shopper value → download or waitlist (gated on NEXT_PUBLIC_DOWNLOADS_ENABLED)
/downloadStore links or waitlist form depending on the same flag
/for-brandsBrand recruitment — features, plans, how-it-works, FAQ; CTAs deep-link to brand.drape.to/apply
/joinReferral 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:

apply / signup / login

login handoff: #token=…

brand.drape.to
(reserved host)

Auth + onboarding

{slug}.drape.to
/auth/handoff

Authenticated dashboard

  • Reserved host (brand.drape.to) serves the marketing landing, /apply onboarding 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 wildcard is live at DNS; Amplify verified=false is a quirk

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.

Brand pricing ladder

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 frontend is not the authorization boundary

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 localStorage with a client-side isTokenExpired() guard (30s skew) that redirects to /login on 401/expiry. Keys differ per app (drape_brand_token vs drape_admin_token).
  • Deploy: AWS Amplify in us-east-1 on the WEB_COMPUTE platform (main = prod). Git connections are drape-ai-llc/* (org), not KCB1099/*. Verified app IDs and domains are on the Infrastructure page.
  • Backend: NEXT_PUBLIC_BACKEND_URLapi.drape.to (dev dev-api.drape.to). staging.drape.to is not in the live Route 53 zone map.