Guides

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

On this page