Last updated 2026-06-29
Quick Start
Get a working Workspaces instance and run a real authorization decision in
about five minutes. You will bring up the service with Docker Compose, list
a user's workspaces, create a team, add a member, run Check,
and share a resource with a single tuple — all against the Connect JSON API.
1. Start the stack
The repo ships a docker-compose.yml that brings up Postgres and
the workspaces service together. Set
GATEWAY_SERVICE_AUTH_TOKENS to one or more service credentials
your product backend will present; here we use a single token.
# Add a dev service token to the workspaces service in docker-compose.yml:# GATEWAY_SERVICE_AUTH_TOKENS=dev-service-token# (the bundled compose sets GATEWAY_DEFAULT_PROJECT_ID=local)docker compose up -d
# health probescurl http://localhost:8080/healthzcurl http://localhost:8080/readyz
The bundled docker-compose.yml sets
GATEWAY_POSTGRES_AUTO_MIGRATE=true so the dev stack migrates on
first connect (it defaults to false — migrations are an
explicit operator step in production). Export the service token so the curls
below can reuse it:
export WS_SERVICE_TOKEN=dev-service-token2. List alice's workspaces
Every RPC is a POST to /workspace.v1.<Service>/<Method>
with the request message as a JSON body. Management RPCs require
acting_user_id — the user the action is authorized as.
ListWorkspaces auto-provisions that user's
PERSONAL workspace on first call.
curl -X POST http://localhost:8080/workspace.v1.WorkspaceService/ListWorkspaces \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"acting_user_id":"alice"}'{ "workspaces": [ { "id": "ws_personal_alice", "projectId": "default", "slug": "alice", "displayName": "alice", "type": "WORKSPACE_TYPE_PERSONAL", "ownerUserId": "alice" } ]}3. Create a team workspace
Create a TEAM workspace named "Acme". The acting user becomes
its owner.
curl -X POST http://localhost:8080/workspace.v1.WorkspaceService/CreateWorkspace \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"acting_user_id":"alice","display_name":"Acme","slug":"acme"}'{ "workspace": { "id": "acme", "projectId": "default", "slug": "acme", "displayName": "Acme", "type": "WORKSPACE_TYPE_TEAM", "ownerUserId": "alice" }}
Ownership is recorded as the tuple
workspace:acme#owner@user:alice, so a Check against
the workspace namespace honours it immediately.
4. Add bob as a member
Add bob to Acme at ROLE_MEMBER.
AddMember writes workspace:acme#member@user:bob.
curl -X POST http://localhost:8080/workspace.v1.WorkspaceService/AddMember \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"acting_user_id":"alice","workspace_id":"acme","user_id":"bob","role":"ROLE_MEMBER"}'5. Check the decision
Ask whether bob is a member of Acme — then whether he is an
admin. Check evaluates the namespace's userset-rewrite rules
transitively and answers allowed.
# workspace:acme#member@bob -> allowedcurl -X POST http://localhost:8080/workspace.v1.AuthzService/Check \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"namespace":"workspace","object_id":"acme","relation":"member","subject_user_id":"bob"}'{ "allowed": true }# workspace:acme#admin@bob -> denied (member is not admin)curl -X POST http://localhost:8080/workspace.v1.AuthzService/Check \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"namespace":"workspace","object_id":"acme","relation":"admin","subject_user_id":"bob"}'{ "allowed": false } member unions in admin and owner, so
alice (the owner) passes a member check too — but the reverse does not hold,
so bob's admin check is denied.
6. Share a resource with a tuple
The engine is generic, not limited to workspaces. Grant bob
direct viewer access to a product object doc1 in
the resource namespace, then check it.
curl -X POST http://localhost:8080/workspace.v1.AuthzService/WriteRelationTuples \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "updates": [ { "op": "OPERATION_INSERT", "tuple": { "namespace": "resource", "object_id": "doc1", "relation": "viewer", "subject": { "user_id": "bob" } } } ] }'curl -X POST http://localhost:8080/workspace.v1.AuthzService/Check \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"namespace":"resource","object_id":"doc1","relation":"viewer","subject_user_id":"bob"}'{ "allowed": true }
That single tuple — resource:doc1#viewer@user:bob — is the
whole grant. No per-resource table, no schema change.
Next steps
- Authorization Model
— how
Checkevaluates rewrites - Workspaces & Groups — the product surface over the engine
- Live API Reference — every RPC and message