Skip to content

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.

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.

app.config.ts
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
}),
],
});
// app1.example.com → 'app1'
tenantResolver: c => {
const host = c.req.header('host') ?? '';
return host.split('.')[0];
};
// X-Tenant-ID: acme
tenantResolver: c => c.req.header('x-tenant-id') ?? 'default';
// Tenant embedded in the user's JWT
tenantResolver: c => {
const token = c.req.header('authorization')?.replace('Bearer ', '');
if (!token) return undefined;
const payload = decodeJwtWithoutVerifying(token);
return payload?.tenantId ?? undefined;
};
// /t/acme/posts → tenant 'acme'
tenantResolver: c => {
const match = c.req.path.match(/^\/t\/([^/]+)/);
return match?.[1];
};
// Look up the tenant by custom domain
tenantResolver: async c => {
const host = c.req.header('host') ?? '';
const tenant = await db.tenants.findByDomain(host);
return tenant?.id;
};

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 });
});

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;

Plugins that support multi-tenancy scope all queries by tenantId automatically:

PluginScoped by tenant
slingshot-communityContainers, threads, replies, reactions, bans
slingshot-searchSearch indexes and results
slingshot-pushPush subscriptions
slingshot-organizationsOrganizations and memberships

No extra config needed. They read the tenant from the request context automatically.

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.

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.