> Every UQL docs page, as Markdown: https://uql-orm.dev/llms.txt
> The same docs over MCP: https://uql-orm.dev/mcp
> Before writing UQL code, read the skill: https://uql-orm.dev/.well-known/agent-skills/uql-orm/SKILL.md

# Multi-tenancy

> Scope every query and write to the current tenant with a security filter and a per-request context.

Source: https://uql-orm.dev/multi-tenancy

A `security` [filter](https://uql-orm.dev/querying/filters.md) scopes every query to the current tenant, read from a per-request context. Reads, writes, relations and cascades are all scoped, so you never write `WHERE tenantId = ...` or set the tenant on a new row by hand.

This page covers tenants sharing a table, kept apart by a column. To give each tenant its own schema instead, see [Multiple Schemas](https://uql-orm.dev/multiple-schemas.md).

## 1. Add a security filter

The filter’s `where` is a function of the context:

```ts
import { Entity, Id, Field, Filter } from 'uql-orm';

@Filter('tenant', {
  where: (ctx) =>
    ctx?.tenantId != null ? { companyId: ctx.tenantId } : undefined,
  security: true,
})
@Entity()
export class Invoice {
  @Id({ type: Number }) id?: number;
  @Field({ type: Number }) companyId?: number | null;
  @Field({ type: Number }) total?: number | null;
}
```

With no tenant, return `undefined`, never `{ companyId: undefined }`, so the query throws instead of running unscoped.

To type `ctx.tenantId`, declare the context once. Keep the keys optional, since a background job may know only some of them:

```ts
declare module 'uql-orm' {
  interface UqlContext {
    tenantId?: number;
    system?: boolean;
  }
}
```

## 2. Set the context

`withContext` sets the context for a unit of work. It follows `await`, `Promise.all`, transactions and [pool calls](https://uql-orm.dev/querying/querier.md#choosing-poolx-vs-querierx):

```ts
import { withContext } from 'uql-orm';

await withContext({ tenantId: 42 }, async () => {
  await pool.findMany(Invoice, { $where: { total: { $gt: 100 } } });
  // SELECT ... FROM "Invoice" WHERE ("total" > $1) AND ("companyId" = $2)   -- $2 = 42
});
```

At your HTTP boundary, `getContext` does this for every request. Read the tenant from a verified source such as a session or a decoded JWT, never from client input:

```ts
import { createRequestHandler } from 'uql-orm/http';

createRequestHandler<{ user: { tenantId: number } }>({
  pool,
  include: [Invoice],
  getContext: (req) => ({ tenantId: req.user.tenantId }),
});
```

[NestJS](https://uql-orm.dev/nestjs.md) takes the same option: `UqlModule.forRoot<AuthedRequest>({ pool, getContext })`.

## What `security: true` guarantees

A security filter is always applied: `{ filters: false }` and bypassing by name are ignored, and the HTTP request parser accepts only query keys, so a client cannot send `filters` or `context`. Its condition is ANDed on its own, so a request sending `$where: { companyId: 999 }` becomes `companyId = 999 AND companyId = 42` and matches nothing.

It fails closed. When the condition returns `undefined`, the query throws `UqlSecurityError`, and `onMissing: 'skip'` is refused on a security filter.

It follows relations. `$populate: { company: true }` enforces `company`’s security filter on every driver, MongoDB included, and so do [relation filters and `$size` counts](https://uql-orm.dev/querying/relations.md).

It guards writes:

- An insert gets the filter’s fields from the context, and naming another value is refused.
- An update cannot change them.
- A save or upsert naming another tenant’s row fails on its key (a `409` over HTTP) instead of overwriting it.
- A condition returning `{}` writes freely. One that is not plain field equalities (`$in`, `$or`) refuses writes, since there is no single value to check.

A refused write throws `UqlSecurityError`, a `403` over HTTP.

Security filters combine with other filters. An `Invoice` that is also [soft-deletable](https://uql-orm.dev/entities/soft-delete.md) gets both conditions, `WHERE ("companyId" = $1) AND ("deletedAt" IS NULL)`, and `withDeleted()` lifts only the soft-delete one.

## Jobs, scripts and tests

Outside an HTTP request, wrap the work in `withContext({ tenantId }, () => runNightlyBilling(tenantId))`.

For trusted work across all tenants, such as startup recovery, give the filter’s `where` a system branch that returns `{}` and run those jobs under a system context. A query with no context at all still fails closed. Annotate the function’s return type: typing the const as `FilterWhere<Invoice>` instead lets a mistyped column through.

```ts
import type { QueryWhere, UqlContext } from 'uql-orm';

// the filter's `where`
const tenantScope = (ctx?: UqlContext): QueryWhere<Invoice> | undefined =>
  ctx?.system
    ? {}
    : ctx?.tenantId != null
      ? { companyId: ctx.tenantId }
      : undefined;

await withContext({ system: true }, () => recoverStaleJobs());
```

## Event-driven pipelines (emitters, timers, queues)

`withContext` uses `AsyncLocalStorage`, which follows `await` but not event callbacks. An emitter listener, a timer or a queued job runs outside the scope that registered it. `captureContext()` captures the context and replays it when the callback fires:

```ts
import { captureContext } from 'uql-orm';

const scoped = captureContext(); // inside the scoped request
deepgram.on('transcript', (t) => scoped(() => saveTranscript(t)));
```

When the code already knows its tenant, pass `{ context }` to `withQuerier` or `transaction` instead:

```ts
await pool.withQuerier(
  (q) => q.updateMany(Resource, { $where: { id } }, patch),
  { context: { tenantId } },
);
```

Wrap your app’s few chokepoints (event bus, queue runners, socket dispatch), not every function.

> **Adopt it fully**
>
> The filter replaces passing `tenantId` through every `$where` and insert by hand. Remove the manual scoping when you adopt it, or you keep two sources of truth for one rule.

## Security filters and database RLS

Security filters hold only inside UQL. Raw SQL (`querier.all`) is yours to scope, and other services on the same database bypass them. For the strongest isolation, add the database’s own row-level security, such as [Postgres RLS](https://uql-orm.dev/postgres.md#row-level-security), as a backstop.
