# Tenants, membership, permissions and subscription

Routed from `skills/SKILL.md`. The authoritative operation list is
[api-reference.md](api-reference.md).

## What is served

- `bun.tenant.create`, `bun.tenant.current`
- `bun.membership.invite`, `bun.membership.accept`, `bun.membership.revoke`
- `bun.permission.check`
- `bun.subscription.require_active`

## What is not served

Lines below are marked `NOT-SERVED:` — the guard test asserts each named
operation is genuinely absent from the registry, so this list cannot go stale by
quietly naming something that later gets implemented.

NOT-SERVED: `bun.tenant.delete` `bun.tenant.list` `bun.tenant.update`
NOT-SERVED: `bun.permission.grant` `bun.permission.revoke` `bun.permission.list`
NOT-SERVED: `bun.membership.list` `bun.subscription.create` `bun.subscription.cancel`

Permission assignment is not an operation. A sibling family having a verb is not
evidence that this family has it.

## Isolation is enforced below the operation layer

Tenant isolation is not an application-code check that an operation performs and
could forget. It is PostgreSQL **FORCE row-level security** with policies, and it
is exercised under a dedicated non-owner `NOBYPASSRLS` role — a superuser
connection is refused at connect time. Four databases carry tenant-scoped tables
(`bun_backend`, `bun_connector`, `bun_workflow`, `bun_context`), and every
tenant-scoped table is enumerated from the PostgreSQL catalogue, so a newly added
table cannot slip through unprotected.

Two consequences for anyone writing against this:

1. **A write path that stores a NULL `tenant_id` breaks isolation even though the
   policy is correct.** Cross-tenant tests here now assert both halves — the other
   tenant is denied *and* the owning tenant still succeeds. Ten tests once asserted
   only the first half, which a `USING (false)` policy passes while breaking the
   product for everyone.
2. **SQLite is never evidence for any of this.** A green test on SQLite says
   nothing about RLS.

Operations that declare `requires_tenant: yes` fail closed when tenant context
cannot be derived. Consult the `tenant` column in
[api-reference.md](api-reference.md) rather than guessing — several families
(`deploy`, `credential`, `webhook`) deliberately do not require it.
