NUMU Docs
Contact
APIThemes
Partner Apps
APIThemes
Partner Apps
  1. Storefront host
  • šŸ—‚ļø Themes overview
  • šŸš€ Start here
  • Glossary
  • Theme engine
    • Architecture
    • BYOT contract
    • Customizer
    • Federation runtime
    • Page data contract
    • Theme manifest
  • Theme SDK
    • Components
    • Federation helpers
    • Hooks
    • SDK overview
    • Type definitions
  • CLI & Vite plugin
    • CLI commands
    • CLI overview
    • Lint rules
    • Section library
    • Vite plugin
  • Storefront host
    • API proxies
    • Built-in fallbacks
    • BYOT fork
    • Routing
    • Storefront overview
  1. Storefront host

Storefront overview

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
It does not own:
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.
Top-level routes:
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
Full reference: Routing.

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.
Proxy categories:
PathHelperPurpose
/api/cart/*cart-proxy.tsCart mutations
/api/customer/*customer-proxy.tsAuth + /me
/api/checkoutdirect fetchSingle-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}/relateddirect fetchPublic read
/api/image-transformdirect fetchImage resize service
Detailed reference: API Proxies.

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
Detailed reference: Built-in Fallbacks.

Caching#

Cache layerWhatTTL
Next.js Data CachefetchStoreByDomain60s
Next.js Data CachefetchProductBySlug60s (tag: product:{storeId}:{slug})
Next.js Data CachefetchProducts(storeId)60s (tag: products:{storeId})
ISRHome /60s (Phase 4.7)
ISRPDP /products/[slug]300s (Phase 4.7)
Application Redis (NUMU-api)Store by subdomain60s
CDN (Cloudflare)Static assetsindefinite
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:
VarExampleUsed for
NUMU_API_URLhttps://api.numueg.app (no /api/v1 suffix)Backend base URL
NUMU_PLATFORM_DOMAINnumueg.app (prod), localhost (dev)Subdomain resolution
NEXT_PUBLIC_NUMU_ENVproduction / staging / test / developmentFeature flags + URL helpers
R2_BUCKET_URLhttps://r2.numueg.appTheme bundle CDN
IMAGE_TRANSFORM_BASE(Cloudflare Image Resizing URL)<Image> srcset target
Don't suffix /api/v1
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.

Performance budget#

Targets (Phase 4.7 + 5.9):
TTFB for home: < 200ms (p95)
LCP for home: < 1.5s (3G)
FCP for PDP: < 1.0s
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.
Modified atĀ 2026-09-24 13:03:12
Previous
Routing
Built with