Multi-Tenancy
Slingshot has a first-class tenant model. Every request carries a tenantId on the context. Plugins that support multi-tenancy — community, search, push, organizations — scope their data by that ID automatically. Provide one function that extracts the tenant from the request; the rest wires itself.
How it works
Section titled “How it works”The framework calls your tenantResolver on every request before any routes run. The returned string is available via getRequestTenantId(c) in all middleware and route handlers. Plugins read it automatically.
For single-tenant apps: tenantResolver: () => 'default'.
For multi-tenant apps: return a different string per tenant — from the subdomain, a header, a JWT claim, or a database lookup.
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: { 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'tenantResolver: c => { const host = c.req.header('host') ?? ''; return host.split('.')[0];};Header
Section titled “Header”// X-Tenant-ID: acmetenantResolver: c => c.req.header('x-tenant-id') ?? 'default';JWT claim
Section titled “JWT claim”// Tenant embedded in the user's JWTtenantResolver: c => { const token = c.req.header('authorization')?.replace('Bearer ', ''); if (!token) return undefined; const payload = decodeJwtWithoutVerifying(token); return payload?.tenantId ?? undefined;};Path prefix
Section titled “Path prefix”// /t/acme/posts → tenant 'acme'tenantResolver: c => { const match = c.req.path.match(/^\/t\/([^/]+)/); return match?.[1];};Database lookup
Section titled “Database lookup”// Look up the tenant by custom domaintenantResolver: async c => { const host = c.req.header('host') ?? ''; const tenant = await db.tenants.findByDomain(host); return tenant?.id;};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