The numu-storefront Next.js 16 app ā what every customer hits at <sub>.numueg.app.Responsibilities#
The storefront's job is to:1.
Resolve the store from the request's Host header (subdomain ā store_id)
2.
Resolve the theme the merchant has installed (BYOT bundle URL, or fall back to built-in)
3.
SSR the route with whatever data the bundle needs in page.data
4.
Proxy mutations to NUMU-api (cart writes, customer auth, checkout)
5.
Render ā either the theme bundle (BYOT) or the built-in fallback
Business logic (taxes, shipping, payments) ā that's NUMU-api
Theme rendering decisions ā that's the theme bundle
Customizer state ā that's the merchant hub
Admin functions ā that's the hub + numu-admin
Request flow#
Browser
ā GET https://acme.numueg.app/products/blue-shirt
ā¼
Cloudflare (proxied, Flexible SSL ā origin HTTP)
ā
ā¼
nginx (port 80 / per-subdomain server block)
ā proxy_pass to numu-storefront:3000
ā¼
Next.js middleware (src/proxy.ts)
ā - parse subdomain "acme" ā x-numu-host header
ā - check store.password_protected ā maybe redirect to /password
ā - apply locale prefix (/ar/products/x ā /products/x with locale cookie)
ā¼
App Router ā [domain]/products/[slug]/page.tsx
ā 1. fetchStoreByDomain("acme") ā store_id
ā 2. fetchProductBySlug(store_id, "blue-shirt")
ā 3. fetchThemeSettings(store_id)
ā 4. resolveByotFork(domain, { type: "product", data: { product } })
ā¼
ByotThemeBoundary OR BuiltInProductDetail
Multi-tenancy via subdomains#
The middleware (src/proxy.ts) stamps x-numu-host on every incoming request. Downstream API client + proxy routes use this header to determine which store to scope to.acme.numueg.app ā x-numu-host: acme.numueg.app
shop.acme.com (custom) ā x-numu-host: shop.acme.com (custom domains map via API)
acme.localhost:3000 ā x-numu-host: acme.localhost (dev)
acme.test.numueg.app ā x-numu-host: acme.test.numueg.app (test env)
The resolution path: host ā fetchStoreByDomain ā store record from API is cached for 60s. Misses 404 the request (the customer sees notFound()).Routing#
All store-scoped routes live under app/[domain]/. The [domain] segment is filled by the middleware via a rewrite() ā customers never see /acme/products/x, they see /products/x.app/[domain]/
āāā page.tsx home
āāā products/[slug]/page.tsx PDP
āāā collections/[slug]/page.tsx PLP
āāā cart/page.tsx cart
āāā checkout/
ā āāā page.tsx contact
ā āāā shipping/page.tsx
ā āāā payment/page.tsx
ā āāā review/page.tsx
ā āāā processing/page.tsx
ā āāā [order_id]/thank-you/page.tsx
āāā account/
ā āāā login/, register/, recover/, reset/
ā āāā orders/, orders/[id]/
ā āāā addresses/, profile/, gift-cards/
ā āāā page.tsx dashboard
āāā search/page.tsx
āāā blogs/, blogs/[handle]/, blogs/[blog]/[article]/
āāā policies/[handle]/page.tsx
āāā pages/[handle]/page.tsx
āāā password/page.tsx pre-launch gate
āāā error.tsx, loading.tsx, not-found.tsx
āāā sitemap.xml, robots.txt
The BYOT fork#
Every route resolves the store + theme, then forks:// pseudo-code from real routes
const themeSettings = await fetchThemeSettings(store.id);
if (themeSettings.external_theme?.bundle_url && !isBuiltInTheme(themeSettings.theme_id)) {
return <ByotThemeBoundary page={{ type: "product", data: { product } }} ... />;
}
return <BuiltInProductDetail product={product} />;
Helper at src/lib/byot-fork.tsx. Detailed reference: BYOT Fork.API proxies#
The storefront's app/api/ is a thin proxy layer over NUMU-api. Why proxy:1.
Cookie scope. Customer cookies are scoped to the storefront domain, not the API.
2.
CSRF double-submit. Cart + customer writes verify the numu_csrf cookie matches the x-numu-csrf header.
3.
Idempotency. Stamps idempotency-key headers on mutations.
4.
Subdomain ā store_id resolution. Themes call /api/cart/add without knowing the store_id; the proxy resolves it.
| Path | Helper | Purpose |
|---|
/api/cart/* | cart-proxy.ts | Cart mutations |
/api/customer/* | customer-proxy.ts | Auth + /me |
/api/checkout | direct fetch | Single-shot order creation |
/api/storefront/* | customer-proxy.ts (CSRF off) | Public reads (apps, currencies, pickup-locations) |
/api/gift-cards/{code} | customer-proxy.ts (CSRF off) | Public balance check |
/api/products/{id}/related | direct fetch | Public read |
/api/image-transform | direct fetch | Image resize service |
Built-in fallbacks#
When a store has no BYOT theme installed (fresh stores) and no built-in template configured (vanilla bazar theme without a customizer setup), the storefront's built-in components render. Recently:BuiltInProductDetail.tsx ā variant picker, qty, add-to-cart
BuiltInCart.tsx ā line items with variant labels, qty controls, applied-promotion, totals
Caching#
| Cache layer | What | TTL |
|---|
| Next.js Data Cache | fetchStoreByDomain | 60s |
| Next.js Data Cache | fetchProductBySlug | 60s (tag: product:{storeId}:{slug}) |
| Next.js Data Cache | fetchProducts(storeId) | 60s (tag: products:{storeId}) |
| ISR | Home / | 60s (Phase 4.7) |
| ISR | PDP /products/[slug] | 300s (Phase 4.7) |
| Application Redis (NUMU-api) | Store by subdomain | 60s |
| CDN (Cloudflare) | Static assets | indefinite |
The revalidate directives on each page file control ISR. Tag-based revalidation fires on backend mutations (e.g. publishing a new theme version revalidates theme:{storeId}).Environment#
The most important env vars for the storefront:| Var | Example | Used for |
|---|
NUMU_API_URL | https://api.numueg.app (no /api/v1 suffix) | Backend base URL |
NUMU_PLATFORM_DOMAIN | numueg.app (prod), localhost (dev) | Subdomain resolution |
NEXT_PUBLIC_NUMU_ENV | production / staging / test / development | Feature flags + URL helpers |
R2_BUCKET_URL | https://r2.numueg.app | Theme bundle CDN |
IMAGE_TRANSFORM_BASE | (Cloudflare Image Resizing URL) | <Image> srcset target |
The storefront code appends /api/v1 itself. Setting NUMU_API_URL=https://api.numueg.app/api/v1 doubles the path and silently falls back to "NUMU Store" defaults for every tab title.
Targets (Phase 4.7 + 5.9):TTFB for home: < 200ms (p95)
LCP for home: < 1.5s (3G)
Total JS for built-in routes: < 80KB gz
Theme bundle: enforced by the marketplace at < 200KB gz per bundle
Bundles over budget fail marketplace review.