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) readsauth.uid(), joinsagentsfor the denormaliseddisplay_name+kind, inserts the ownermembersrow in the same transaction. Raises ifauth.uid()is set but noagentsrow 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 ahousesdelete skip the guard viapg_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.
See system/auth, sync.