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
Userpool, 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; askCheck"may this user do this?" andExpand"who can?" It is generic over namespaces, so any consuming product expresses its own access model as tuples. - Multi-tenant projects.
project_idis the isolation shard (identity ADR-0002): every workspace, group, invitation, and tuple is scoped to it. Empty means the configuredGATEWAY_DEFAULT_PROJECT_ID.
What it is not
- Not an authenticator. It authorizes; it does not verify
end-user identity. It trusts the
acting_user_idthe product backend passes, because the backend already verified the user at the edge. - Not a per-product schema. There are no
workspace_membersordoc_sharestables — 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.
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 tuplesSurface 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
- Quick Start — docker compose up,
then your first
Check - Authorization Model — tuples, namespaces, and rewrites
- Security & Service Auth — the service credential and error codes