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 notadmin,api, ordocsare 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/adminand 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:
- Request arrives (e.g.
GET https://mysite.com/). - The rewrite rule in
next.config.mjsapplies when the path does not matchadmin,api, ordocs. The host is captured and the request is rewritten to/:tenantDomain/:path*. - Layout/data fetching looks up the tenant by domain via
payload.find({ collection: 'tenants', where: { domain: { equals: tenantDomain } } }). - 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.
Per-tenant branding & legal
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
.localor.testTLDs (Chrome force-redirects.devto HTTPS). - The tenant
domainmust 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.