Platform guides
02 · Members + roles

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. The last owner cannot be demoted or removed (sole-owner invariant — see Section 6).
  • 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

POST /v1/workspaces/:slug/invites
curl https://api.platform-auth.productcraft.co/v1/workspaces/acme/invites \
  -H 'authorization: Bearer <platform-access-token>' \
  -H 'content-type: application/json' \
  -d '{
    "email":   "newhire@acme.com",
    "role":    "admin",
    "expires_in_hours": 168
  }'

Requires workspace.member.invite (owner + admin by default) and a human session — PAKs get 403. The response contains a code; when email is set, the invite is locked to that address and ProductCraft emails the recipient an accept link. The recipient signs in (or signs up) at auth.productcraft.co and accepts the code to join. The invite expires after expires_in_hours (168 = 7 days); revoke earlier via DELETE .../invites/:id. Only an owner can invite another owner.


3

Change a member's role

PATCH /v1/workspaces/:slug/members/:id/role
curl -X PATCH https://api.platform-auth.productcraft.co/v1/workspaces/acme/members/<account-id>/role \
  -H 'authorization: Bearer <platform-access-token>' \
  -H 'content-type: application/json' \
  -d '{ "role": "admin" }'

Pass role for a system role or role_id (UUID) for a custom role. 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:

POST /v1/workspaces/:slug/roles
curl https://api.platform-auth.productcraft.co/v1/workspaces/acme/roles \
  -H 'authorization: Bearer <platform-access-token>' \
  -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": ["*"]
      }
    ]
  }'

Role authoring requires workspace.role.create (owner-only by default) and a human session — PAKs get 403.


5

The IAM-style policy model

The same shape as AWS IAM. Statements have effect (allow|deny), actions, and resources. Explicit deny wins.

Action strings — <service>.<resource>.<verb>:

workspace.settings.update  auth.create          mail.send
workspace.delete           auth.audit.read      mail.suppression.manage
workspace.member.invite    waitlist.update      social.read
workspace.role.create      waitlist.export      social.flag
workspace.audit.read       ...                  ...

The full catalog is queryable: GET /v1/workspaces/:slug/policies/actions/catalog. Allow statements naming unknown actions are rejected with 400.

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 narrowing

Wildcards 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 SOLE_OWNER.
  • Removing an owner from membership when they're the sole owner → 409 SOLE_OWNER.
  • 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. Account deletion auto-promotes the oldest admin (or oldest other member if no admins exist) to owner, so the workspace is never left ownerless.


7

Remove a member

DELETE /v1/workspaces/:slug/members/:id
curl -X DELETE https://api.platform-auth.productcraft.co/v1/workspaces/acme/members/<account-id> \
  -H 'authorization: Bearer <platform-access-token>'

Requires workspace.member.remove on the caller's role. Two refusal cases:

  • Sole owner — 409 SOLE_OWNER (see Section 6).
  • Last member — 409 LAST_MEMBER. Decommission the whole workspace instead via DELETE /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.