Permissions

Two roles per org_members row: owner and member. Roles are scoped per-org — a user can be owner in one org and member in another.

Helpers

src/lib/auth/session.ts:

  • requireSession() — any valid member. Returns { userId, email, orgId, defaultOrgId, role } or null.
  • requireOwnerSession() — owner-only. Returns null if the user's role in the active org isn't owner.

Both read the active-org cookie (sendoka_active_org) so the resolved orgId reflects the sidebar switcher, not the sign-in default.

Where owner is enforced

Endpoint Effect
POST /api/internal/api-keys create key
DELETE /api/internal/api-keys?id= revoke key
POST /api/internal/domains add domain
DELETE /api/internal/domains?id= remove domain
POST /api/internal/webhooks create endpoint
DELETE /api/internal/webhooks?id= remove endpoint
POST /api/internal/webhooks/rotate rotate secret
POST /api/internal/templates create template
DELETE /api/internal/templates?id= delete template
POST /api/internal/team/invite invite a member
PATCH /api/internal/team/members change member role
DELETE /api/internal/team/members?id= remove a member
POST /api/internal/billing upgrade (checkout session)

Non-mutating reads (GET on the same paths) require just requireSession() — members can see what exists.

Guards baked into member management

  • Cannot demote the user referenced by organizations.owner_id.
  • Cannot remove the user referenced by organizations.owner_id.
  • Cannot remove yourself — "ask another owner." Prevents accidental lockout.
  • First owner (the user who created the org) is always safe.

403 vs 401

  • 401 Unauthorized — no valid session at all.
  • 403 Owner role required — logged in but not owner for the active org.

Gaps

  • Finer-grained permissions (read-only on specific surfaces, sub-roles like "billing admin") not yet implemented. Single-tier owner/member is deliberately simple for now.
  • No server-side check that hides owner-only dashboard controls from members. Currently members see the buttons but get a 403 when clicking — add a role fetch to the dashboard layout and conditionally render.