Architecture
How a single Next.js deployment serves many tenants resolved by hostname.
Nordcom Commerce is a single Next.js deployment that serves many tenants. Tenants are resolved by hostname in middleware before any page renders. No per-tenant build, no per-tenant deploy — one binary, many storefronts.
Request flow
flowchart LR
A[Request] --> B[Middleware]
B --> C[Shop.findByDomain]
C --> D[App Router]
D --> E[Shopify / CMS]The middleware at apps/storefront/src/proxy.ts is the entry point for every request:
- Paths starting with
/admindispatch toadmin()insrc/middleware/admin.ts; everything else goes tostorefront(). storefront()readsreq.headers.host, normalizes it (strips ports,.localhost, Vercel preview suffixes), and callsShop.findByDomain(hostname), which resolves the tenant through the Convexdb/shopsquery seam.- On a hit, the resolved domain is injected into the URL so the App Router serves the
page from
src/app/[domain]/[locale]/…. - On
NotFoundError, the middleware rewrites toSERVICE_DOMAIN/status/unknown-shop/. Other commerce errors rewrite to/status/unknown-error/. - Locale is resolved after the shop — see Locales for the full fallback chain.
The App Router never sees an un-tenanted request. Every route lives under
[domain]/[locale]/… — there are no root-level routes that need to figure out which
shop they are serving.
Data layer
Data lives in a Convex deployment (packages/convex owns the schema and server
functions). App access goes through @nordcom/commerce-db — the only workspace that
talks to the Convex db/* functions; every other consumer imports the high-level service
classes (ShopService, UserService, ReviewService, FeatureFlagService) from there.
@nordcom/commerce-db is marked server-only. Importing it from a client component or
calling a service without CONVEX_URL in the environment will throw.
Commerce layer
Shopify Storefront and Admin APIs sit behind AbstractApi in
apps/storefront/src/utils/abstract-api.ts. Build one via
ShopifyApolloApiClient({ shop, locale }) — never call Apollo directly from a route.
The @inContext(country, language) directive is injected automatically by the Apollo
DocumentTransform from @nordcom/commerce-shopify-graphql. Source operations must not
pre-declare these arguments — the transform owns them.
Content layer
Structured content (pages, articles, navigation, product/collection metadata overlays)
lives in Convex; @nordcom/commerce-cms defines the content shapes (field descriptors),
the editor the admin app mounts at /cms, and the storefront's block render layer. The
storefront reads content through the Convex cms/read functions. See
CMS for the block model and block loader pattern.
Error layer
A shared, code-tagged error hierarchy lives in @nordcom/commerce-errors. Every thrown
error in the platform is an instance of one of these classes — never new Error(...).
Each class carries a stable code, an HTTP statusCode, and a help URL. See
Errors for the full class inventory and how to add new errors.
Cache layer
Tag-based cache invalidation is handled by the @tagtree package family. Tag schemas are
declared as typed TypeScript objects; builders and invalidators are derived from them.
Every entity loader in the storefront tags its cached payload; Shopify webhooks and CMS
afterChange hooks call revalidateTag to flush exactly the affected entries. See
Caching for the tag schema and namespace details.