# RLS and multi-tenancy

Every Fayz app is multi-tenant: many customers (tenants) share the same database. Isolation between them is **not** enforced in application code, and does **not** come from separating schemas — it's enforced **in the database**, with Postgres Row Level Security (RLS). RLS **is** the model's isolation boundary: one tenant's row is physically invisible to the others, even if the application gets it wrong.

## The single rule: `tenant_id` + RLS

The rule is the same across every layer of the [data model](/en/docs/data/model) — core, plugin `plg_` tables and your app's tables: every table carries a `tenant_id` column and has an RLS policy keyed on it. The **canonical** form scopes the row to the tenants the logged-in user belongs to:

```sql
CREATE POLICY tenant_isolation ON <table>
  USING (tenant_id IN ( /* tenants of the logged-in user */ ));
```

The user's set of tenants comes from a function in the `public` schema, resolved from the user's membership in each tenant. The policy applies to SELECT, INSERT, UPDATE and DELETE. The result: a query never sees a row from a tenant the user doesn't belong to — Postgres filters it out before the application ever sees it.

{% callout type="info" %}
The **concept** — a policy scoped by `tenant_id` to the user's set of tenants — is stable. The **exact name** of the function and the literal SQL text depend on the installed package wave and their labels may change; treat the SQL above as the shape, not as a fixed signature.
{% /callout %}

## Website and admin: same tables, different roles

Tenant isolation is only the first axis. The second is **who, inside the tenant, sees what** — and that lives in RLS too, over the **same tables**. The site side and the admin side don't have separate tables (`public_*`/`admin_*`); they have **different policies and roles** over the same table: `staff` sees the tenant's rows, the end customer sees only their own. That's why Fayz never duplicates tables per surface — one table, several views, all guaranteed by the database.

## Canonical vs. deferred

Not every plugin writes the policy inline, and that's intentional:

- **canonical** — the policy writes the canonical form (`tenant_id` scoped to the user's set of tenants) directly in the plugin's migration.
- **deferred** — the plugin enables RLS (`ENABLE ROW LEVEL SECURITY`) but does **not** create the policy; the table (with a `tenant_id` column, type `BASE TABLE`, no `_` prefix) is picked up by an auto-detection block in the app, which emits **exactly** the canonical policy at apply time.

A deferred plugin therefore **lands as canonical** in a real database — it isn't a divergence, just a deferral of *where the policy text lives*. Both forms produce the same isolation. Any other form (`divergent`, `no-rls`) is treated as a problem to standardize before merging.

## Your app's tables isolate too

The tables your app or incubator creates are no exception. A customers table or an appointments table you add **must** carry `tenant_id` and have RLS. It's easy to forget, because those tables are yours; the rule applies to them just the same — and to plugins' `plg_` tables.

## Migrations are append-only once applied

One operational detail that avoids silent corruption: **a migration that has already run against any real database is immutable.** Fix it with a *new* migration, never by editing the file — the file would silently drift from the database.

The exception is before the first apply: a greenfield app, or a plugin's source schema that hasn't run yet, can be edited in place (it's even preferable — a fresh apply should produce the final shape directly). Rule of thumb: *already touched a live database? → new migration. Never applied? → edit in place.*

{% callout type="tip" %}
You almost never write these policies by hand. `fayz db apply` emits the canonical form for you, and the model's table pattern (core, `plg_`, your tables) already ensures every new table lands isolated. What this page gives you is the *why* — so you can recognize a badly isolated table when you see one.
{% /callout %}

---

Next: how the logged-in user is resolved in [Auth — overview](/en/docs/auth/overview).
