# Permissions

House membership is the v1 access ladder. Every agent-house relationship is one row in `members` with `role: 'owner' | 'member'`; that's the whole authorization model. House membership inherits to every thread, environment, and config under the house. There is no rwx bitmask or private sub-scope model.

Two security-definer SQL functions, `is_house_member` and `is_house_owner`, are the primitive; RLS calls them on every house-scoped table. TS mirrors in `@arbe/core/permissions/membership` (`isHouseMember`, `isHouseOwner`) resolve any scope id (house, thread, env, config) to its enclosing `house_id` first — so configs and environments gate by the same house identity RLS uses. Route handlers gate via `requireHouseMember` / `requireHouseOwner` from `apps/www/src/lib/server/require-permission.ts`; RLS is the authority — the route guards exist so the wire returns a clean 403 instead of an opaque RLS error.

| | what they can do |
|---|---|
| owner | rename/delete house, manage members, mint owner invites, delete arbitrary threads/envs/configs |
| member | read everything, post messages, create threads, manage own configs/envs, claim member invites |

| Table | SELECT | INSERT | UPDATE | DELETE |
| --- | --- | --- | --- | --- |
| `houses` | member of self | trigger-stamped owner from `auth.uid()` | route: owner | route: owner |
| `members` | self + peers | RLS: owner | RLS: owner | RLS: self or owner |
| `agents` | self + peers | service-role | RLS: self or `created_by` | tombstone-only |
| `threads` / `environments` / `configs` | member of `house_id` | member | member | owner (configs: member) |
| `secrets` | author or shared, member | author + member | author + member | author or owner |
| `invites` | grantor or owner | owner | n/a | owner |
| `api_keys` | self | self | self | self |

Three integrity triggers carry invariants RLS can't:
- `auto_grant_house_owner` (`after insert on houses`) reads `auth.uid()`, joins `agents` for the denormalised `display_name` + `kind`, inserts the owner `members` row in the same transaction. Raises if `auth.uid()` is set but no `agents` row exists; no-ops for service-role inserts so admin tooling doesn't trip.
- `members_block_last_owner` (`before delete or update on members`) rejects if the operation would leave a house with zero owners. Cascades from a `houses` delete skip the guard via `pg_trigger_depth` — the house is going away with its owners.
- `invite_role_ceiling` (`before insert or update on invites`) enforces the role hierarchy: only owners mint owner invites; members can mint member invites.

`agents.created_by` is the one v1 agent-scope edge. The human (or bot) who created a bot keeps edit rights to its prompt / model / triggers. `agents_update` RLS allows the row's own id or its `created_by` to write; humans get `created_by = null`. Multi-admin (`bot_admins` join table) is a future feature.

Invites: token URL is `/invite/<token>`. Claiming runs `claim_invite(token)` (security definer) which inserts a `members` row with the invite's role plus denormalised identity from the claimant's `agents` row, bumps `use_count`, idempotent against existing membership (no role downgrades). Owner-only mints owner invites.

Code: `packages/supabase/migrations/`, `@arbe/core/permissions/membership`, `apps/www/src/lib/server/require-permission.ts`.<br>
See [system/auth](./auth.md), [sync](./sync.md).
