Multi-tenancy & request context
Thread tenant and request metadata through hooks, plugins, and transactions.
better-drizzle does not impose a multitenancy model, but it gives you enough surface to thread request metadata through cleanly — and to enforce tenant scoping with a plugin when you want it automatic.
Attach metadata with meta
Every operation accepts a meta object. Use it to carry correlation and tenant context:
await client.users.findMany({
where: { active: true },
meta: {
requestId: 'req_123',
tenantId: 'tenant_42',
userId: 'admin_7',
},
});Read it in hooks:
const client = better(db, {
schema,
hooks: {
beforeQuery(ctx) {
console.log(ctx.meta?.tenantId, ctx.meta?.requestId);
},
},
});Scope default metadata with $withContext(...)
Passing meta on every call works, but gets repetitive when the same context applies to many operations. $withContext clones the client with default metadata that is automatically merged into every subsequent operation:
const scoped = client.$withContext({
requestId: 'req_123',
tenantId: 'tenant_42',
});
// All operations on `scoped` carry the metadata automatically
await scoped.users.findMany({
where: { active: true },
});Per-call meta still wins on key conflicts:
await scoped.users.create({
data: { name: 'Alice' },
meta: {
requestId: 'req_override',
userId: 'admin_7',
},
});The returned client is a clone — the original client is unaffected. Nested $withContext calls stack, and the scoped metadata is inherited into transactions.
Transaction context
Transactions accept a context object, readable by hooks and plugins as transactionContext:
await client.transaction(
async (tx) => tx.users.findMany(),
{ context: { requestId: 'req_123', tenantId: 'tenant_42' } },
);Enforcing tenant scope automatically
meta is for observation. To enforce that every query is scoped to a tenant, use a plugin transform that injects the tenant filter, combined with $withState to pass the current tenant per request:
transform(operation) {
const tenantId = operation.state.tenantId;
if (!tenantId) return operation;
if (!operation.model.hasColumn('tenantId')) return operation;
operation.where = (
operation.where
? { AND: [operation.where, { tenantId }] }
: { tenantId }
) as typeof operation.where;
return operation;
}// per request
const scoped = client.users.$withState({ tenantId: currentTenant });
await scoped.findMany();Where this fits
- tenant-aware audit logging
- request tracing with correlation IDs
- consistent tenant scoping across repository calls and raw SQL