Multi-Tenancy
Multi-Tenancy
Section titled “Multi-Tenancy”ecommus is built for multi-tenancy from day one. A single deployment can serve unlimited independent stores, each with their own products, orders, customers, and settings — completely isolated from each other.
Tenant Resolution
Section titled “Tenant Resolution”Every HTTP request that touches tenant data must go through the resolveTenant() middleware, which sets req.tenantId before any route handler runs.
Resolution order:
X-Tenant-Idheader (for programmatic API access)- Subdomain:
mystore.ecommus.app→ tenant slugmystore - Custom domain lookup in
settingstable
export async function resolveTenant(req: FastifyRequest, reply: FastifyReply) { const header = req.headers['x-tenant-id'] as string | undefined; const subdomain = extractSubdomain(req.hostname);
const tenantId = header ?? await lookupTenantByDomain(subdomain);
if (!tenantId) { return reply.code(400).send({ error: 'tenant_not_found' }); }
req.tenantId = tenantId;}Query-Level Isolation
Section titled “Query-Level Isolation”Every database query in a route handler must include a tenantId filter. This is enforced by code review and the requireTenant preHandler.
// ✅ Correct — all data filtered by tenantconst products = await db.select() .from(schema.products) .where(and( eq(schema.products.tenantId, req.tenantId), // ← required isNull(schema.products.deletedAt) ));
// ❌ Forbidden — cross-tenant data leak (OWASP A01)const products = await db.select().from(schema.products);Middleware Stack for Admin Routes
Section titled “Middleware Stack for Admin Routes”All admin routes use this preHandler chain:
{ preHandler: [requireAuth, resolveTenant, requireTenant]}requireAuth— verifies JWT, setsreq.userresolveTenant— resolves and setsreq.tenantIdrequireTenant— returns 403 ifreq.tenantIdis not set
Database Schema
Section titled “Database Schema”Every table that holds tenant-specific data has a tenantId column:
-- All tenant-scoped tables share this patterntenant_id TEXT NOT NULL,-- plus a composite index for performance:INDEX idx_products_tenant (tenant_id, deleted_at)See packages/db/src/schema/ for all table definitions.
Storefront Routing
Section titled “Storefront Routing”The storefront resolves the tenant from the domain name at request time:
mystore.ecommus.app → X-Tenant-Id: mystore → API calls filter by tenantIdIn development, use X-Tenant-Id: demo header or configure TENANT_ID=demo in .env.
Plugin Isolation
Section titled “Plugin Isolation”Plugins also receive tenantId via PluginContext. Any plugin that stores settings or data must scope it to the tenant:
// Plugin using settings APIconst settings = await ctx.settings.get('my-plugin:api-key', req.tenantId);Plugin settings in the settings table use the key pattern plugin:<name>:settings and are stored as a JSON blob per tenant.