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.

docker compose
# 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 probes
curl http://localhost:8080/healthz
curl 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-token

2. 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 -> allowed
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":"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