# Multi-tenancy & request context

> Thread tenant and request metadata through hooks, plugins, and transactions.

URL: https://better-drizzle.com/docs/guides/multi-tenancy

better-drizzle does not impose a multitenancy model. For isolation, the [tenant plugin](/docs/plugins/tenant) scopes every query to a tenant. This page covers the other half: threading tenant and request metadata through hooks, plugins, and transactions for logging and tracing.

## Attach metadata with `meta`

Every operation accepts a `meta` object. Use it to carry correlation and tenant context:

```ts
await client.users.findMany({
	where: { active: true },
	meta: {
		requestId: 'req_123',
		tenantId: 'tenant_42',
		userId: 'admin_7',
	},
});
```

Read it in [hooks](/docs/advanced/hooks):

```ts
const client = better(db, {
	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:

```ts
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:

```ts
await scoped.users.create({
	data: { email: 'alice@example.com', 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](/docs/advanced/transactions) accept a `context` object, readable by hooks and plugins as `transactionContext`:

```ts
await client.transaction(
	async (tx) => tx.users.findMany(),
	{ context: { requestId: 'req_123', tenantId: 'tenant_42' } },
);
```

## Enforcing tenant isolation

`meta` and `$withContext` are for *observation*: they never filter rows. To *enforce* that every query is scoped to a tenant, use the [tenant plugin](/docs/plugins/tenant):

```ts
import { tenant } from 'better-drizzle/tenant';

const client = better(db, {
	plugins: [tenant({ columns: 'organizationId', global: ['plans'] })],
});

// per request
const scoped = client.$tenant({ organizationId: currentTenant });
await scoped.projects.findMany({ include: { tasks: true } });
```

The plugin filters through core model scopes, so the tenant predicate reaches relation loads, relation filters, `count`, relation sorts, writes, upserts, and the lookups behind `connect`, `disconnect`, and `set`. It stamps tenant columns on create, fails closed without a tenant, denies raw SQL on scoped clients by default, and can set PostgreSQL row-level security settings.

Combine both: `$tenant()` scopes the data, and `$withContext()` (or `meta`) carries the request ids your hooks log.

```ts
const scoped = client
	.$tenant({ organizationId: currentTenant })
	.$withContext({ requestId, userId });
```

> **Root-only transforms are not isolation:** A [plugin transform](/docs/plugins/writing-plugins#transforms) that adds
> `{ tenantId }` to `operation.where` only changes the root query. Relation
> loads, relation filters, `count`, relation writes, and raw SQL run without
> it, and a call without the tenant runs unfiltered. Use the tenant plugin, or
> declare a [model scope](/docs/plugins/plugin-scopes#model-scopes) in your
> own plugin, when tenants must never see each other's data.

## Where this fits

* tenant-aware audit logging
* request tracing with correlation IDs
* tenant isolation with the [tenant plugin](/docs/plugins/tenant)
