Skip to Content
ArchitectureMulti-Tenancy

Multi-Tenancy

ShipMore runs multi-tenant on a single box: one Next.js process, one MongoDB, one Stripe account, unlimited tenants. Each tenant is identified by its domain.

App segments

The app is split into three areas:

  • (frontend) — Tenant sites. Requests whose path is not admin, api, or docs are rewritten so the host becomes the first route segment: /:tenantDomain/:path*. All tenant pages and data are scoped to that tenant.
  • (payload) — Admin panel and Payload API. Served at /admin and under /api. Not subject to tenant rewrites.
  • /docs — This documentation (Nextra). Excluded from tenant rewrites so the same docs are served regardless of host.

Domain → tenant resolution

Tenancy is host-based. The incoming request host (e.g. mysite.com or localhost:3000) is used to find a tenant document in the tenants collection (by the domain field).

Flow:

  1. Request arrives (e.g. GET https://mysite.com/).
  2. The rewrite rule in next.config.mjs applies when the path does not match admin, api, or docs. The host is captured and the request is rewritten to /:tenantDomain/:path*.
  3. Layout/data fetching looks up the tenant by domain via payload.find({ collection: 'tenants', where: { domain: { equals: tenantDomain } } }).
  4. The tenant document drives theme, site name, SEO defaults, and any tenant-scoped queries.

Tenant scoping in the service layer

Tenant-scoped collections use the @payloadcms/plugin-multi-tenant plugin.

The service layer receives tenantId as an explicit argument. It does not resolve “which tenants can this user access?” — that complexity belongs to the Payload RBAC layer. The service uses the tenantId it’s given.

// ✅ correct await PageService.addBlock({ pageId, block, tenantId, user }) // ❌ wrong — service should not resolve tenant from user await PageService.addBlock({ pageId, block, user })

Per-tenant Orama indexes

The Orama search index is scoped per tenant and lives in-process as a Node module-level singleton. See Data → Search & Explore.

Each tenant document holds:

  • Site name + site logo
  • Theme (one of 22 built-in themes — see Extras)
  • Header / Footer / Banner layout configuration
  • Legal pages (terms, privacy, license — Lexical rich text)
  • Default SEO (title, description, OG image, favicon)
  • Analytics (GA ID, custom scripts)

Local dev with multiple tenants

Use /etc/hosts to map aliases to 127.0.0.1, then create one tenant per alias. See Configure Tenants for the step-by-step.

Caveats:

  • Use .local or .test TLDs (Chrome force-redirects .dev to HTTPS).
  • The tenant domain must include the port for local dev (e.g. tenant1.local:3000).
  • Cookies are domain-scoped — signing in on one tenant does not carry to another.

Production routing

  • Railway — add each tenant domain as a Custom Domain on the same service; CNAME to Railway’s target.
  • Vercel — every tenant domain must be added as a project Custom Domain; A/CNAME per Vercel’s instructions.

In both cases, traffic for any custom domain hits the same app, and the host-based rewrite resolves the tenant. See Deployment.