Add your team. Scope what they can do.
System roles (owner/admin/member) cover most teams. Custom roles let you grant 'invoice exporter' or 'support reader' without making someone an admin.
1
System roles
Three roles ship with every workspace:
- owner — every permission in the catalog. Includes destructive verbs like
workspace.delete. Cannot be deleted; can't be the only owner (sole-owner invariant — see Section 5). - admin — every operational permission. Excludes role-management (create/update/delete roles, assign roles to members) and destructive verbs like
workspace.delete. Admins manage the day-to-day; owners shape who-can-do-what. - member — read-only across most surfaces + personal API keys.
2
Invite a member
curl https://api.platform-auth.productcraft.co/v1/workspaces/acme/invites \
-H 'authorization: Bearer <cookie-or-pak>' \
-H 'content-type: application/json' \
-d '{
"email": "newhire@acme.com",
"role": "admin",
"expires_in_hours": 168
}'Sends an invite email via Mail (when enabled on your workspace). Recipient signs up at auth.productcraft.co/signup and hits the invite code to join. After 168 hours the invite expires; revoke earlier via DELETE .../invites/:id.
3
Change a member's role
curl -X PATCH https://api.platform-auth.productcraft.co/v1/workspaces/acme/members/<account-id>/role \
-H 'authorization: Bearer <cookie-or-pak>' \
-H 'content-type: application/json' \
-d '{ "role": "admin" }'Owner-only — only system owners can change roles. Admins explicitly cannot (the sole-owner invariant requires the role graph itself to be owner-managed).
4
Custom roles
When the three system roles don't fit — e.g. you want a “billing-exporter” role that can read but not write, and can also export the message log:
curl https://api.platform-auth.productcraft.co/v1/workspaces/acme/roles \
-H 'authorization: Bearer <cookie-or-pak>' \
-H 'content-type: application/json' \
-d '{
"name": "billing-exporter",
"description": "Read invoices + export message log. No write.",
"policy": [
{
"effect": "allow",
"actions": ["mail.message.read", "mail.message.body.read", "workspace.audit.read"],
"resources": ["*"]
}
]
}'5
The IAM-style policy model
The same shape as AWS IAM. Statements have effect (allow|deny), actions, resources, optional conditions. Explicit deny wins.
Action strings — <service>.<verb>:
workspace.update auth.create mail.send
workspace.delete auth.audit.read mail.suppression.manage
workspace.invite.create waitlist.update social.read
workspace.role.create waitlist.export social.flag
workspace.audit.read ... ...Resource URNs — pcft:<scope>[/<id>]:
* # workspace-wide
pcft:workspace/<workspaceId> # the workspace itself
pcft:heimdall:app/<appId> # per-Auth-app narrowing
pcft:agora:community/<commId> # per-Social-community narrowingWildcards work in any segment — auth.* matches every Auth verb; pcft:heimdall:app/* matches every app in the workspace. Trailing * swallows the rest.
Caller-narrowing. When creating or updating a role, the caller's own policy must include every action they're trying to grant. An admin (whose policy excludes workspace.delete) can't hand the role they're editing a workspace.delete permission. Defence against self-escalation.
6
Sole-owner invariant
A workspace must always have at least one owner. The API refuses transitions that would leave the workspace ownerless:
- Demoting an owner to admin/member when they're the sole owner → 409
WOULD_REMOVE_LAST_OWNER. - Removing an owner from membership when they're the sole owner → 409.
- The sole owner can't leave the workspace via self-leave; they must promote someone else first OR delete the workspace entirely.
Edge case: deleting your account (not the workspace) when you're a sole owner. AccountService.delete auto-promotes the oldest admin (or oldest non-self member if no admins exist) to owner. Documented in the platform-auth-api CLAUDE.md — and S-10 in the API security review notes the unfortunate silent-promotion behaviour that we'll fix with a notification email.
7
Remove a member
curl -X DELETE https://api.platform-auth.productcraft.co/v1/workspaces/acme/members/<account-id> \
-H 'authorization: Bearer <cookie-or-pak>'Requires workspace.member.remove on the caller's role. Two refusal cases:
- Sole owner — 409 (see Section 6).
- Last member — 409
WOULD_LEAVE_WORKSPACE_EMPTY. Decommission the whole workspace instead viaDELETE /v1/workspaces/:slug.
Self-leave is allowed without workspace.member.remove — members can always leave their own workspaces, subject to the same sole-owner + last-member invariants.