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 }ornull.requireOwnerSession()— owner-only. Returnsnullif the user's role in the active org isn'towner.
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
rolefetch to the dashboard layout and conditionally render.