Multi-Tenancy
Slingshot has a first-class tenant model. Production apps explicitly declare single- or multi-tenant mode; there is no anonymous production fallback.
How it works
Section titled “How it works”Single-tenant mode stamps one configured identity centrally:
tenancy: { mode: 'single', tenantId: 'primary' }Multi-tenant mode extracts an ID from a header, subdomain, or path and requires
onResolve to validate it. The resolved identity is available through
getRequestTenantId(c) and is carried through registered package boundaries.
Setting up a tenant resolver
Section titled “Setting up a tenant resolver”import { defineApp } from '@lastshotlabs/slingshot';import { createAuthPlugin } from '@lastshotlabs/slingshot-auth';import { createCommunityPackage } from '@lastshotlabs/slingshot-community';
export default defineApp({ port: 3000, security: { signing: { secret: process.env.JWT_SECRET!, }, }, tenancy: { mode: 'multi', resolution: 'subdomain', onResolve: async tenantId => { if (!tenantId || tenantId === 'www') return null; return { slug: tenantId }; }, }, plugins: [ createAuthPlugin({ auth: { roles: ['admin', 'member'], defaultRole: 'member', }, db: { auth: 'memory', sessions: 'memory', oauthState: 'memory', }, }), ], packages: [ createCommunityPackage({ containerCreation: 'user', // No tenantResolver needed here — it reads from context automatically }), ],});Tenant resolution strategies
Section titled “Tenant resolution strategies”Subdomain
Section titled “Subdomain”// app1.example.com → 'app1'tenancy: { mode: 'multi', resolution: 'subdomain', onResolve };Header
Section titled “Header”// X-Tenant-ID: acmetenancy: { mode: 'multi', resolution: 'header', headerName: 'x-tenant-id', onResolve };Path prefix
Section titled “Path prefix”// /t/acme/posts → tenant 'acme'tenancy: { mode: 'multi', resolution: 'path', pathSegment: 1, onResolve };onResolve is where database lookup, disabled-tenant rejection, and tenant
configuration loading occur. Return null to reject without trusting the
caller-provided identifier.
Reading the tenant in routes
Section titled “Reading the tenant in routes”tenantId lands on c.var before any route handler runs:
import type { AppEnv } from '@lastshotlabs/slingshot-core';import { createRouter, getRequestTenantId } from '@lastshotlabs/slingshot-core';
const router = createRouter();
router.get('/my-data', c => { const tenantId = getRequestTenantId(c); // string | null return c.json({ tenantId });});Per-tenant config
Section titled “Per-tenant config”Store tenant config (feature flags, plan limits, etc.) alongside the ID:
tenantResolver: async c => { const host = c.req.header('host') ?? ''; const tenant = await tenantCache.get(host); if (!tenant) return undefined;
// tenantConfig is also set on the context c.set('tenantConfig', { plan: tenant.plan, maxUsers: tenant.maxUsers, features: tenant.features, });
return tenant.id;};Read it anywhere:
const config = c.get('tenantConfig') as { plan: string; maxUsers: number } | null;Plugin data isolation
Section titled “Plugin data isolation”Plugins that support multi-tenancy scope all queries by tenantId automatically:
| Plugin | Scoped by tenant |
|---|---|
slingshot-community | Containers, threads, replies, reactions, bans |
slingshot-search | Search indexes and results |
slingshot-push | Push subscriptions |
slingshot-organizations | Organizations and memberships |
No extra config needed. They read the tenant from the request context automatically.
Auth in multi-tenant apps
Section titled “Auth in multi-tenant apps”Auth is not tenant-scoped by default — a user account exists across all tenants. To scope roles per tenant, use TenantRole:
import type { AuthAdapter } from '@lastshotlabs/slingshot-core';
declare const adapter: AuthAdapter & { addTenantRole(userId: string, tenantId: string, role: string): Promise<void>;};
await adapter.addTenantRole('usr_123', 'acme', 'member');With tenant roles enabled, getActor(c).roles holds the user’s roles scoped to the current tenant.
Organizations as tenants
Section titled “Organizations as tenants”When using slingshot-organizations, organizations are a natural tenant boundary:
tenantResolver: c => { // After auth middleware sets the actor, resolve the user's current org const orgId = c.req.header('x-organization-id'); return orgId ?? undefined;};Users switch organizations by changing the X-Organization-ID header. Their auth token stays the same.
See also
Section titled “See also”- Auth Setup — session and role configuration
- Forum App — community scoped to containers
- SaaS Foundations — organizations as tenant boundaries
- Horizontal Scaling — sharing tenant data across instances