ReBAC Model
Platform membership roles, environment deployers, and how SpiceDB enforces actions — separate from app end-user roles.
Control Plane authorization is relationship-based (ReBAC), enforced by SpiceDB. Role names like owner and member are stored as relationships; API handlers check a closed action vocabulary against resource types.
Three role planes (do not mix)
| Plane | Who | Where you manage it | Enforced by |
|---|---|---|---|
| Platform membership | Portal users on accounts/workspaces | Members & Roles | SpiceDB portal_* |
| Environment deployer | Portal users allowed to deploy to an env | SpiceDB portal_environment (often via workspace admin) | SpiceDB |
| Workspace site auth (app roles) | End users of your product | Authentication | Per-workspace IdPlane / external IdP |
Platform and app roles share labels like “admin” in casual speech — they are not the same grants.
Resource types
| SpiceDB type | Maps to | Typical scope |
|---|---|---|
portal_user | Human portal user | Subject of platform checks |
portal_account | Account (billing org) | Top-level tenant |
portal_workspace | Workspace | Data + apps + coderunners boundary |
portal_environment | Environment | Deploy / promote target |
Types are prefixed portal_ because SpiceDB is shared with Dataplane tenant schemas.
Platform membership roles
Users hold roles on accounts and workspaces via SpiceDB relationships (synced from memberships rows):
| Role | Typical scope | Grants (conceptually) |
|---|---|---|
owner | account, workspace | Full administration |
billing_admin | account only | Billing + account administer |
admin | account, workspace | Administer resources and members |
writer | account, workspace | Read + write + delete on data pillars |
member | account, workspace | Read access |
partner | account, workspace | Read-only external collaborator (portal membership) |
Exact permission math lives in the SpiceDB schema. Handlers never trust the role string alone — they call check(action, …).
Environment deployer
| Relation | On | Grants |
|---|---|---|
deployer | environment | deploy on that environment (plus anyone with workspace administer) |
This is a platform relationship for deploy/promote targets — not an app end-user role.
Closed action vocabulary
Handlers call check(action, resource_type, resource_id) with one of:
| Action | Valid on |
|---|---|
administer | account, workspace |
read | account, workspace, environment |
write | workspace |
delete | workspace |
ddl | workspace |
deploy | workspace, environment |
manage_members | account, workspace |
view_billing | account |
A 403 means SpiceDB denied the action — the JWT was valid but the user lacks the relationship.
Adding a new action requires a schema change, authorization code update, and documentation — all in the same change. See SpiceDB schema.
How checks run
| Path | SpiceDB consulted? |
|---|---|
GET /v1/workspaces/{id} | Yes — per-record check |
GET /v1/workspaces (my workspaces) | Indirect — filtered via memberships table |
| Workspace pillar APIs | Yes — action per endpoint |
Control Plane talks to SpiceDB via direct gRPC — not through Dataplane.
Customer Portal mapping
| Portal area | Typical action |
|---|---|
| Accounts list | read on account |
| Create workspace | administer on account |
| Data Vault write | write on workspace |
| Secrets reveal | read + audited |
| Members add/revoke | manage_members |
| Mint LLM virtual key | administer on workspace |
| Workspace auth settings | administer on workspace |
Related
- SpiceDB schema — full Zed definitions
- Members & Roles — platform membership UI
- Authentication — app end-user roles
- API Authentication — JWT + 403 behavior
- Memberships API