> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Row-Level Security

> Check and enable PostgreSQL Row-Level Security (RLS) for tenant data isolation across all platform tables.

<Info>
  **Scope of this API vs. production RLS state.** This endpoint (and the
  `RLS_TABLES` registry behind it, `src/lib/db/rls.ts`) only covers
  Prisma-modeled tables that were registered in that array before 2026-07-10.
  As of 2026-07-10, RLS is enabled and forced on **all 111 target tables in
  production** — this endpoint's registry plus 51 additional tables (e.g.
  `consent_records`, `webauthn_credentials`, `shopify_shops`,
  `tenant_settings`) that were enabled directly via
  `prisma/manual-sql/35_rls_track_a.sql` and are not tracked by this API.
  Tables with a `tenantId`/`tenant_id` column got the standard
  `tenant_isolation` policy described below; tables without one (e.g.
  NextAuth tables, child tables FK'd to a tenant-scoped parent) got
  default-deny RLS instead. Supabase's Data API (the anon/PostgREST HTTP
  surface that could otherwise reach any of these tables directly,
  bypassing the app) has also been disabled at the source.

  **This does not mean RLS governs the app's own queries.** The app's
  Postgres connection uses a `BYPASSRLS`-class role by design, so none of
  these policies constrain what the application itself can read or write —
  they only stop the anon/PostgREST path and any other non-bypass
  connection. Tenant isolation for the app's own traffic is still enforced
  at the application layer (the `where: { tenantId }` convention, backed by
  isolation tests) via `requireTenant()`. `withTenantRLS()` (`src/lib/db/rls.ts`)
  exists as an opt-in helper for scoping a transaction's session variable,
  but it has no production call sites yet — app-query-level RLS enforcement
  is a separate, deliberately-not-yet-executed initiative.
</Info>

## GET /api/v1/admin/rls

Check RLS status on the tables tracked by this API (`RLS_TABLES` in
`src/lib/db/rls.ts`). Returns which of those tables have RLS enabled,
forced, and which have the `tenant_isolation` policy. This is a subset of
all RLS-protected tables in production — see the scope note above.

### Response

```json theme={null}
{
  "summary": {
    "totalTables": 28,
    "rlsEnabled": 28,
    "rlsForced": 28,
    "withPolicy": 28,
    "missingRLS": []
  },
  "tables": [
    {
      "table": "offers",
      "rlsEnabled": true,
      "rlsForced": true,
      "policies": ["tenant_isolation"]
    },
    {
      "table": "interaction_history",
      "rlsEnabled": true,
      "rlsForced": true,
      "policies": ["tenant_isolation"]
    }
  ],
  "timestamp": "2026-03-30T12:00:00.000Z"
}
```

### Summary Fields

| Field         | Type      | Description                                          |
| ------------- | --------- | ---------------------------------------------------- |
| `totalTables` | integer   | Total number of tenant-scoped tables                 |
| `rlsEnabled`  | integer   | Tables with RLS enabled                              |
| `rlsForced`   | integer   | Tables with FORCE RLS (applies even to table owners) |
| `withPolicy`  | integer   | Tables with the `tenant_isolation` policy            |
| `missingRLS`  | string\[] | Table names missing RLS or the isolation policy      |

### Roles

admin only

***

## POST /api/v1/admin/rls

Enable RLS on all tables tracked by this API (`RLS_TABLES`). This is idempotent and safe to call multiple times. For each table, it:

1. Enables RLS (`ALTER TABLE ... ENABLE ROW LEVEL SECURITY`)
2. Forces RLS for table owners (`ALTER TABLE ... FORCE ROW LEVEL SECURITY`)
3. Creates a `tenant_isolation` policy scoped to the session variable `app.current_tenant_id`:

   ```sql theme={null}
   CREATE POLICY tenant_isolation ON "<table>"
   USING ("tenantId" = (SELECT current_setting('app.current_tenant_id', true)))
   WITH CHECK ("tenantId" = (SELECT current_setting('app.current_tenant_id', true)))
   ```

   The `current_setting(...)` call is wrapped in a scalar subquery so
   Postgres evaluates it once per query (an `InitPlan`) instead of once per
   row. `current_setting(..., true)` returns `NULL` when the variable isn't
   set, and `NULL` never equals a tenant ID — so a connection with no tenant
   context set sees zero rows (fail-safe), and the `WITH CHECK` clause
   applies the same rule to inserts/updates.

### Response

```json theme={null}
{
  "success": true,
  "enabled": ["offers", "interaction_history", "decision_traces"],
  "failed": [],
  "totalTables": 28,
  "timestamp": "2026-03-30T12:00:00.000Z"
}
```

### Response Fields

| Field         | Type      | Description                               |
| ------------- | --------- | ----------------------------------------- |
| `success`     | boolean   | `true` if no tables failed                |
| `enabled`     | string\[] | Tables where RLS was successfully enabled |
| `failed`      | string\[] | Tables where RLS enablement failed        |
| `totalTables` | integer   | Total number of tenant-scoped tables      |

An audit log entry is created for the operation.

### Roles

admin only

<Warning>
  RLS enforcement depends on the PostgreSQL session variable `app.current_tenant_id` being set on a connection (via `set_config('app.current_tenant_id', ..., true)`) before that connection's queries can be tenant-scoped by these policies. **The platform's own Prisma client does not set this variable** — it connects with a `BYPASSRLS`-class role, so these policies don't constrain it either way. This session variable only matters for connections that are both non-bypass *and* have RLS enabled/forced on the tables they query (e.g. the anon/PostgREST path, or a future non-bypass role under Track B). Tenant isolation for the app's own queries is enforced by the application-layer `where: { tenantId }` convention, not by this session variable.
</Warning>

See also: [Admin](/api-reference/admin) | [Security](/governance-security/security)
