Roles are stored per user per organization (OrganizationMembership.role). Enum values: OWNER, EDITOR, VIEWER.
Ingest remains authenticated with project API keys, not user sessions.
| Capability | VIEWER | EDITOR | OWNER |
|---|---|---|---|
| Read telemetry (dashboard GETs scoped to project) | Yes | Yes | Yes |
Resolve / unresolve error groups (PATCH /api/errors/:id) |
No | Yes | Yes |
List API keys (GET /api/project/api-keys) |
Yes | Yes | Yes |
Create API key (POST /api/project/api-keys), optional allowedApp for per-app restriction |
No | Yes | Yes |
Revoke API key (POST /api/project/api-keys/:publicId/revoke) |
No | No | Yes |
List organizations (GET /api/meta/organizations) |
Yes | Yes | Yes |
Create organization (POST /api/meta/organizations; you become owner of the new org) |
Yes | Yes | Yes |
Rename organization (PATCH /api/meta/organizations/:orgId) |
No | No | Yes |
Create project (POST /api/meta/projects) |
No | No | Yes |
Rename project (PATCH /api/meta/projects/:projectId) |
No | No | Yes |
Add member / email invite (POST /api/meta/organizations/:orgId/members) |
No | No | Yes |
Change member role (PATCH /api/meta/organizations/:orgId/members/:userId) |
No | No | Yes |
Per-app API keys: If ApiKey.allowed_app is set, ingest requests authenticated with that key must send the same value in the JSON app field on /ingest/event, /ingest/session, /ingest/batch, and /ingest/error. Mismatch returns 403.
Invites: Unknown emails create an OrganizationInvite row; owners receive an invite URL (e.g. /register?invite=…). POST /api/auth/register with inviteToken creates the user with a single OrganizationMembership on that organization at the invite role.
Registration (self-serve, no invite): The new user is created without any OrganizationMembership. They use POST /api/meta/organizations to create a workspace (they become OWNER of that org), then add projects and API keys as usual. TELEMETRY_ALLOW_REGISTRATION controls whether signups are allowed after the first user row exists; the first user row can still be created this way when the database is empty.
API enforcement: Mutations (POST/PATCH that change data or API keys, including PATCH /api/errors/:id, POST /api/project/api-keys, revoke) require a valid session (Authorization: Bearer or session cookie) and then enforce role. Unauthenticated callers receive 401. GET telemetry routes require a session in production (NODE_ENV=production); in development, or when TELEMETRY_ALLOW_UNAUTHENTICATED_READS=true, legacy unauthenticated project scoping remains available for local tooling (see resolveReadProjectId in apps/api/src/lib/read-project-request.ts).
Dashboard: /api/meta/session-context returns role, boolean capability flags, usageQuota, and billingHealth (Stripe subscription snapshot for billing banners when configured) for the active project and sidebar organization (X-Project-Id / X-Organization-Id + session).
Organization scoping: The dashboard may send X-Organization-Id so GET /api/meta/projects returns only projects in that organization (caller must be a member).