Last updated 2026-06-29

Using the API

This page is the conceptual guide to calling the workspaces API: the transport, the service credential, the request fields every RPC shares, and the error model. For the exhaustive, per-RPC reference — every request and response message, generated live from the proto — use the Live API Reference linked below.

Live, auto-generated reference. Browse and try every RPC at /workspace/api (rendered by Scalar), backed by the OpenAPI spec at /workspace/openapi/workspace-v1.yaml. The spec is generated from the proto by make openapi, so it is always in sync with the running service.

Two views, two audiences: the Live API Reference above is the HTTP/JSON surface (with a try-it console), while the Proto Reference is a generated HTML view of the protobuf contract itself — every service, message, enum, and the raw .proto — which is what you generate a gRPC/Connect client from. The canonical, versioned schema and generated SDKs are published to the Buf Schema Registry (buf.build/elloloop/workspace).

Transport

Every endpoint is a Connect RPC. The simplest way to call it is plain HTTP POST with a JSON body — no client library required, just curl. The same handlers also speak gRPC and gRPC-Web, so generated Connect or gRPC clients work unchanged against the same port.

The URL path is fixed by the proto package and is the same for every protocol:

/workspace.v1.<Service>/<Method>

The proto package is workspace.v1, so a call to ListWorkspaces on WorkspaceService is a POST to /workspace.v1.WorkspaceService/ListWorkspaces. By default the service listens on port 8080 (GATEWAY_CONNECT_PORT).

Authentication: the service credential

Workspaces is an internal service-to-service API. The caller is a trusted product backend, not a browser and not an end user. It authenticates with a shared service token sent as a bearer header:

Authorization: Bearer <service-token>

Valid tokens come from the GATEWAY_SERVICE_AUTH_TOKENS environment variable (comma-separated). A missing or wrong credential is rejected with HTTP 401, which Connect surfaces to clients as code Unauthenticated. If GATEWAY_SERVICE_AUTH_TOKENS is empty the requirement is disabled and every caller is trusted — only appropriate behind a service mesh, mTLS, or a private network (see Security & Service Auth).

The end user is data, not the caller. Like Zanzibar, this service never sees an end-user login. The bearer token identifies the calling service; the human the action is performed for is passed explicitly in the request body. End-user authentication happens at the product edge, before this service is called.

Shared request fields

Three fields recur across the API. They carry the user identities and the isolation shard — the service trusts them because it already trusted the caller's service token.

Field On Meaning
acting_user_id Management RPCs (workspace, group, invitation writes/reads) Required. The user the action is authorized as. Omitting it returns InvalidArgument.
subject_user_id Check, ReadRelationTuples The user being tested — pure data, independent of the caller or the acting user.
project_id Every RPC Optional isolation shard. Empty falls back to GATEWAY_DEFAULT_PROJECT_ID (default "default"). See Projects & Multi-Tenancy.

Two worked calls

List the workspaces Alice belongs to (auto-provisions her 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"}'

Ask whether Bob is an admin of workspace W:

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":"W","relation":"admin","subject_user_id":"bob"}'

The response is JSON shaped like the proto response message — for Check, {"allowed": true}.

Error model

Errors use standard Connect error codes. Over HTTP/JSON each maps to an HTTP status and a body with a code and message. The ones you will see:

Connect code HTTP When
unauthenticated 401 Missing or wrong service token in the Authorization header.
invalid_argument 400 A required field is missing or malformed — e.g. no acting_user_id, an empty display_name, or a role of ROLE_OWNER on AddMember.
permission_denied 403 The acting user is not authorized to perform the action on the target.
not_found 404 The referenced workspace, group, member, or invitation does not exist in the project.
failed_precondition 412 The request conflicts with the resource's state — e.g. adding a member to a personal workspace, or accepting an already-consumed invitation.

The three services

Workspaces exposes three Connect services. The Live API Reference documents each request and response field; this table is the map.

Service Owns RPCs
WorkspaceService Workspaces, members, invitations CreateWorkspace, GetWorkspace, ListWorkspaces, UpdateWorkspace, DeleteWorkspace, AddMember, UpdateMemberRole, RemoveMember, ListMembers, CreateInvitation, AcceptInvitation, ListInvitations, RevokeInvitation
GroupService Reusable, nestable membership sets CreateGroup, GetGroup, ListGroups, DeleteGroup, AddGroupMember, RemoveGroupMember, ListGroupMembers
AuthzService The relation-tuple engine WriteRelationTuples, ReadRelationTuples, Check, Expand

ListWorkspaces auto-provisions the caller's personal workspace on first call. CreateInvitation returns a one-time token; ListInvitations never echoes it. ReadRelationTuples is a raw store read with no rewrite evaluation — use Check for an authorization decision and Expand for the resolved userset tree (see Check & Expand).

Generated clients

Go services should not hand-write requests. The repository ships generated Connect-Go clients under gen/go — the message types in gen/go/workspace/v1 and the client/handler interfaces in gen/go/workspace/v1/workspacev1connect. Import them from the module github.com/elloloop/workspace and call typed methods instead of building JSON by hand; the bearer header is set the same way:

import (
workspacev1 "github.com/elloloop/workspace/gen/go/workspace/v1"
"github.com/elloloop/workspace/gen/go/workspace/v1/workspacev1connect"
)
client := workspacev1connect.NewWorkspaceServiceClient(
http.DefaultClient, "http://localhost:8080",
)
req := connect.NewRequest(&workspacev1.ListWorkspacesRequest{ActingUserId: "alice"})
req.Header().Set("Authorization", "Bearer "+serviceToken)
res, err := client.ListWorkspaces(ctx, req)

For other languages, generate a Connect or gRPC client from the same OpenAPI/proto and point it at the same URL and header.