Last updated 2026-06-29

Introduction

Workspaces is an internal authorization microservice (Go + Connect-RPC) backed by Postgres. It owns workspaces, groups, invitations, and a Zanzibar-style relation-tuple engine. This page explains where it sits, how it is called, and the three things it gives you.

The identity / workspace split

Workspaces is the authorization half of the two-service split drawn by identity's ADR-0001. The AuthN/authz seam is a hard line:

  • identity owns authentication, the User pool, and tenancy. It issues the access token that proves who the end user is.
  • workspaces (this service) owns workspaces, workspace membership, groups, invitations, and all fine-grained ReBAC.

The two services never share a table. There is no shared schema, no cross-service join, no synchronous callback from workspaces into identity. The only contact point is the end user's access token — and that token is verified at the product edge, not here. Workspaces performs no end-user token verification: there is no JWKS, no issuer check, no JWT logic in this service at all.

The service-to-service model

The caller of Workspaces is always a trusted product backend, never a browser. The backend authenticates itself with a shared service credential on every request:

Authorization: Bearer <service-token>

Valid tokens come from the GATEWAY_SERVICE_AUTH_TOKENS environment variable (comma-separated). A missing or wrong credential returns HTTP 401 / Connect code Unauthenticated. Leaving the variable empty disables the requirement and logs a warning — for meshes that enforce mTLS at the network layer instead.

Like Zanzibar, the end user is data, not the caller. The user an action is authorized as travels in an explicit acting_user_id field, and the user a permission is tested for travels in subject_user_id. Both are independent of the service credential. End-user authentication already happened at the product edge before this service is called.

What it gives you

  • Workspaces, groups, and invitations (WorkspaceService + GroupService). Every user owns one auto-provisioned personal workspace; they create team workspaces and add members at a role. Groups are reusable, nestable membership sets. Invitations are one-time, token-based grants of a workspace role.
  • A generic relation-tuple engine (AuthzService). Write tuples; ask Check "may this user do this?" and Expand "who can?" It is generic over namespaces, so any consuming product expresses its own access model as tuples.
  • Multi-tenant projects. project_id is the isolation shard (identity ADR-0002): every workspace, group, invitation, and tuple is scoped to it. Empty means the configured GATEWAY_DEFAULT_PROJECT_ID.

What it is not

  • Not an authenticator. It authorizes; it does not verify end-user identity. It trusts the acting_user_id the product backend passes, because the backend already verified the user at the edge.
  • Not a per-product schema. There are no workspace_members or doc_shares tables — every grant is a tuple over one uniform store (see the authorization model).

Who calls it

Product backends call Workspaces — never the browser directly. The browser talks to your backend with the end user's identity token; your backend verifies that token, then calls Workspaces with the service credential plus the acting user as a field.

call path
browser
| end-user identity token (from identity)
v
product backend ── verifies the identity token at the edge
| Authorization: Bearer <service-token>
| body carries acting_user_id (and subject_user_id for Check)
v
workspaces ── workspaces · groups · invitations · relation tuples

Surface area

Three Connect-RPC services — workspace.v1.WorkspaceService, workspace.v1.GroupService, and workspace.v1.AuthzService — over HTTP/JSON (also gRPC and gRPC-Web). Every RPC is a POST to /workspace.v1.<Service>/<Method>. The proto under proto/workspace/v1/workspace.proto is the source of truth.

# Every RPC is a POST to /workspace.v1.<Service>/<Method>
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"}'

Where to next