# tenant

> Scope every query to the request's tenant, on top of RLS.

Source: https://bettersupabase.com/docs/plugins/tenant

```ts
import { tenant } from "better-supabase/plugins/tenant";

const betterSupabase = defineSupabase(schema).use(tenant());
const db = betterSupabase.connect(supabase, { claims }); // or { tenant: organizationId }
```

For tables with the tenant flag:

* reads, updates and deletes get `organization_id = <tenant>`, including
  includes and relation filters;
* inserts get the tenant column filled in;
* writing a row for another tenant, or moving a row to one, fails with
  `forbidden` before anything is sent. A numeric tenant column matches when
  its text equals the tenant;
* an upsert that updates on conflict needs the tenant column in its conflict
  target (`onConflict: ["organizationId", "kvk"]` or a unique constraint that
  includes it), so it cannot overwrite another tenant's row. The SQL and
  SQLite executors also add `where <tenant> = excluded.<tenant>` to the
  update for every table generated with the tenant flag, with or without
  this plugin. PostgREST has no such clause, so on the default executor
  the table's RLS update policy is what stops a cross-tenant upsert.

The tenant it resolves becomes `db.$context.tenant`, so jobs, storage and
cache invalidation that receive `db.$context` use the same tenant.

The tenant comes from `context.tenant`, then the JWT claim, or from your own
`resolve(context)`. `claim` takes one or more dotted paths and defaults to
`['tenant_id', 'app_metadata.tenant_id']`: a top-level claim from a custom
access token hook, or `app_metadata` set through the
Auth admin API. Set `claims.tenant` in the config to rename the claim for the
plugin, `current_tenant_id()`, and the storage and realtime policies together.
`user_metadata` is never a source, because users can write it. Server adapters
fill the context from the verified JWT.

A tenant from a URL or request body is only safe after you check it against
the user's memberships. Do that in `resolve`, or pass `{ tenant }` to
`betterSupabase.connect` once it is checked.

## In the API document [#in-the-api-document]

[`defineApi`](/docs/specs) marks the tenant column of every served tenant
table `readOnly`, described as "Set from the request's tenant.", and drops
it from the required fields of request bodies. When `resolve` reads the
tenant from a header, name it in `header` so the document lists it:

```ts
tenant({
  header: "X-Organization",
  resolve: (context) => checkedOrganization(context),
});
```

Each operation on a tenant table then has an `X-Organization` header
parameter, "The tenant the request acts for.", required unless
`onMissing: 'skip'`. The plugin doesn't read the header itself; `resolve`
does.

Pass your [claims type](/docs/auth#typed-claims) to check the paths at
compile time. Only paths to string claims, up to three levels deep, are
accepted:

```ts
const betterSupabase = defineSupabase(schema)
  .claims(Claims)
  .use(
    tenant<v.InferOutput<typeof Claims>>({ claim: "app_metadata.tenant_id" }),
  );
```

## Missing tenant [#missing-tenant]

By default a query on a tenant table without a tenant fails with
`forbidden`: the plugin fails closed. That includes a query on another table
whose includes or relation filters reach a tenant table. Use `onMissing: 'skip'` to leave it
to RLS, or `{ allTenants: true }` per call for admin tooling:

```ts
await adminDb.customers.count({ allTenants: true });
```

> **Not a replacement for RLS**
>
> The plugin makes queries correct and indexes usable; RLS makes them safe. Keep
> a tenant policy on every table. `better-supabase doctor` checks both.