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

API proxies

The Next.js routes under app/api/ are thin proxies that forward to NUMU-api. They own auth, CSRF, idempotency, and subdomain → store_id resolution.

Why proxy#

If the storefront talked directly to https://api.numueg.app/api/v1 from the browser:
1.
Cookie scope wouldn't work. Customer auth cookies are scoped to the storefront domain, not the API.
2.
CORS would need to be wide-open for every storefront subdomain.
3.
CSRF would be hard. Double-submit cookies must be same-origin.
4.
Subdomain resolution would leak. The browser knows the subdomain via Host, but a cross-origin fetch loses it.
Proxying through https://<sub>.numueg.app/api/* solves all four — cookies are first-party, the proxy stamps x-numu-host, and the backend can trust it.

Categories#

Cart writes — /api/cart/*#

PathBackend routeCSRFIdempotency
POST /api/cart/addPOST /storefront/cart/add✅optional
POST /api/cart/removePOST /storefront/cart/remove✅optional
POST /api/cart/updatePOST /storefront/cart/update✅optional
POST /api/cart/discountPOST /storefront/cart/discount✅–
DELETE /api/cart/discountDELETE /storefront/cart/discount✅–
Helper: lib/cart-proxy.ts → proxyCartMutation(req, path, method).

Cart read — /api/cart#

GET direct passthrough to GET /storefront/cart. No CSRF (safe method). Cookie forwarded as-is.

Customer — /api/customer/*#

Auth routes (store-scoped, need subdomain resolution):
PathBackend route
POST /api/customer/registerPOST /storefront/store/{store_id}/auth/register
POST /api/customer/loginPOST /storefront/store/{store_id}/auth/login
POST /api/customer/logoutPOST /storefront/store/{store_id}/auth/logout
POST /api/customer/recoverPOST /storefront/store/{store_id}/auth/recover
POST /api/customer/resetPOST /storefront/store/{store_id}/auth/reset
POST /api/customer/verify-emailPOST /storefront/store/{store_id}/auth/verify-email
POST /api/customer/resend-verificationPOST /storefront/store/{store_id}/auth/resend-verification
/me routes (cookie-scoped, no subdomain resolution needed):
PathBackend route
GET /api/customer/meGET /storefront/me/profile
PATCH /api/customer/mePATCH /storefront/me/profile
POST /api/customer/me/passwordPOST /storefront/me/password
GET /api/customer/me/addressesGET /storefront/me/addresses
POST /api/customer/me/addressesPOST /storefront/me/addresses
PATCH /api/customer/me/addresses/{id}PATCH /storefront/me/addresses/{id}
DELETE /api/customer/me/addresses/{id}DELETE /storefront/me/addresses/{id}
GET /api/customer/ordersGET /storefront/me/orders
GET /api/customer/orders/{id}GET /storefront/me/orders/{id}
POST /api/customer/orders/{id}/reorderPOST /storefront/me/orders/{id}/reorder
GET /api/customer/saved-cardsGET /storefront/me/saved-cards
Helper: lib/customer-proxy.ts → proxyCustomer(req, { backendPath, method, requireCsrf }). Handles cookie + CSRF + subdomain resolution + Set-Cookie passthrough (multi-cookie via getSetCookie()).

Checkout — /api/checkout#

Single endpoint:
POST /api/checkout → POST /storefront/checkout
No CSRF on the proxy itself — the backend's idempotency-key cache prevents double-submission. The Idempotency-Key header is required and stamped by the storefront's ReviewStep client.

Public reads — /api/storefront/*#

PathBackend route
GET /api/storefront/appsGET /storefront/store/{store_id}/apps
GET /api/storefront/apps/{slug}GET /storefront/store/{store_id}/apps/{slug}
GET /api/storefront/currenciesGET /storefront/store/{store_id}/currencies
GET /api/storefront/unlockPOST /storefront/store/{store_id}/password/unlock
GET /api/storefront/pickup-locationsGET /storefront/store/{store_id}/pickup-locations
GET /api/storefront/checkout-configGET /storefront/store/{store_id}/checkout-config
GET /api/storefront/storeGET /storefront/store-by-subdomain/{sub}
All use proxyCustomer(req, { requireCsrf: false }) since they're idempotent reads.

Gift cards — /api/gift-cards/{code}#

GET /api/gift-cards/{code} → GET /storefront/store/{store_id}/gift-cards/{code}
Phase 8.3 balance check. Public read, no auth.

Image transform — /api/image-transform#

GET /api/image-transform?url=&w=&q=&f=
Proxies to the merchant-configured image-resizing service (Cloudflare Image Resizing by default, self-hosted as fallback). The SDK's <Image> builds srcset URLs targeting this proxy.

Other public reads#

PathPurpose
GET /api/products/{id}/relatedSame-category siblings — drives useRelatedProducts
GET /api/shipping/optionsResolve shipping rates for an address (POST shape)

CSRF protocol#

The storefront sets a numu_csrf cookie on first response (a random opaque token). Mutating proxies require the same token echoed in the x-numu-csrf header.
import { verifyCsrf } from "@/lib/csrf";

export async function POST(req: NextRequest) {
  const err = verifyCsrf(req);
  if (err) return NextResponse.json({ error: "csrf_invalid" }, { status: 403 });
  // ... forward
}
Themes that call mutating proxies must:
1.
Read the numu_csrf cookie via document.cookie
2.
Echo it as the x-numu-csrf request header
The SDK's useCart(), useCustomerActions(), etc. do this automatically.

Idempotency-Key protocol#

Mutations that create resources (checkout, cart-add) accept an Idempotency-Key header. The backend caches the response keyed by (store, customer, key) for ~10 minutes. Re-sending the same key returns the cached response — double-clicks don't double-charge.
The SDK + storefront UIs generate UUIDs per user-intended action. Themes that bypass the SDK should generate keys themselves.

Set-Cookie passthrough#

Login + register return a customer_access_token cookie. The proxy forwards the upstream Set-Cookie header to the browser unchanged. The cookie is HttpOnly, Secure (in prod), SameSite=Lax, scoped to the storefront domain.
// Inside proxyCustomer, simplified
const upstream = await fetch(...);
const response = new NextResponse(upstream.body, { status: upstream.status });
for (const c of upstream.headers.getSetCookie()) {
  response.headers.append("Set-Cookie", c);
}
return response;
getSetCookie() is Node 18+ — multi-cookie responses (rare but possible) are preserved.

Body forwarding#

Non-safe methods forward req.text() directly. This preserves any content-type quirks:
const body = SAFE_METHODS.has(method) ? undefined : await req.text();
const headers: Record<string, string> = {};
if (body !== undefined) headers["Content-Type"] = "application/json";
// ...
We never req.json() + re-stringify — that would normalize away type fidelity (multipart, fields with null vs undefined, etc.).

Adding a new proxy#

If you need to expose a new backend route to themes:
1.
Add the route file under app/api/<your-path>/route.ts.
2.
Pick the right helper:
Cart writes → proxyCartMutation
Customer auth or /me → proxyCustomer
Public read → proxyCustomer with requireCsrf: false
Direct passthrough → write your own with fetch + cookie + x-numu-host
3.
Test cookie passthrough (Set-Cookie in response).
4.
Update routing if it's user-facing.
5.
Update API surface so theme devs know the backend route exists.
Modified at 2026-09-24 13:03:12
Previous
Vite plugin
Next
Built-in fallbacks
Built with