Multi-Tenancy
Configure tenant extraction and isolation in the serve({ queries }) runtime.
Multi-Tenancy Isolation
If you need the older builder-chained tenant APIs such as .tenant(...) and .tenantOptional(), see v0.1.x Multi-Tenancy.
Tenant configuration is enforced by the serve({ queries }) runtime and applies to the query definitions you expose there.
Model
- extract a tenant ID from
ctx.auth - reject requests when tenant context is required but missing
- optionally auto-inject tenant filters into query builders in
ctx - reuse the same tenant-aware runtime for
api.execute(...),api.run(...), and HTTP delivery
With mode: 'auto-inject', hypequery injects tenant filters into compatible query builders in context. That is the recommended mode for SaaS applications.
Global tenant configuration
import { initServe } from '@hypequery/serve';
import { z } from 'zod';
type AppAuth = { userId: string; tenantId: string };
const authStrategy = async ({ request }): Promise<AppAuth | null> => {
const token = request.headers['authorization'];
if (!token) return null;
const decoded = await verifyToken(token);
return { userId: decoded.sub, tenantId: decoded.organization_id };
};
const { query, serve } = initServe({
auth: authStrategy,
context: () => ({
db: myDatabaseConnection,
}),
});
const getOrders = query({
input: z.object({ status: z.string().optional() }),
query: ({ ctx, input }) =>
ctx.db
.table('orders')
.where('status', 'eq', input.status ?? 'completed')
.select('*')
.execute(),
});
export const api = serve({
tenant: {
extract: (auth) => auth.tenantId,
required: true,
column: 'organization_id',
mode: 'auto-inject',
},
queries: { getOrders },
});Configuration options
extract
Use extract to read a tenant ID from your auth object.
column
Set the database column used for tenant filtering when you use mode: 'auto-inject'.
mode
auto-injectautomatically applies tenant filters to compatible builders in contextmanualleaves tenant filtering up to your query logic
required
When required is true, requests without a tenant ID are rejected.
Auto-inject mode
Auto-inject mode is the safest option because it makes tenant filtering the runtime default:
const { query, serve } = initServe({
context: () => ({
db: myDb,
analyticsDb: myAnalyticsDb,
}),
});
const getUsers = query({
query: ({ ctx }) => ctx.db.table('users').select('*').execute(),
});
export const api = serve({
tenant: {
extract: (auth) => auth.tenantId,
column: 'org_id',
mode: 'auto-inject',
},
queries: { getUsers },
});Manual mode
Use manual mode when you need to control tenant predicates yourself:
const { query, serve } = initServe({
context: () => ({ db: myDb }),
});
const getUsers = query({
query: ({ ctx }) =>
ctx.db
.table('users')
.where('organization_id', 'eq', ctx.tenantId)
.select('*')
.execute(),
});
export const api = serve({
tenant: {
extract: (auth) => auth.tenantId,
mode: 'manual',
},
queries: { getUsers },
});Per-query tenant overrides
Object-style query({ ... }) supports per-query tenant overrides.
const { query, serve } = initServe({
context: () => ({ db }),
});
const orders = query({
query: ({ ctx }) => ctx.db.table('orders').select('*').execute(),
});
const adminStats = query({
tenant: {
required: false,
mode: 'manual',
},
query: ({ ctx }) => {
if (ctx.tenantId) {
return ctx.db.table('stats').where('tenant_id', 'eq', ctx.tenantId).execute();
}
return ctx.db.table('stats').select('*').execute();
},
});
export const api = serve({
tenant: {
extract: (auth) => auth.tenantId,
column: 'tenant_id',
mode: 'auto-inject',
},
queries: { orders, adminStats },
});Use per-query overrides when one query needs different tenant behavior than the global runtime:
required: falsemakes tenant context optional for that querymode: 'manual'disables auto-injection for that query- if no global tenant config exists, include
extractin the per-query override
Standalone query.execute(...) does not run the tenant pipeline. Use api.execute(...), api.run(...), or an HTTP route when tenant enforcement matters.