Last updated 2026-06-29

ADR-0002 — Personal-by-default workspaces, and the personal/team split

Status

Accepted (2026-06-15). Builds on ADR-0001: a workspace is an object in the workspace namespace, and its membership is a set of relation tuples. This ADR decides the product shape of that namespace.

Context

The products this service backs span a spectrum from pure B2C to pure B2B:

  • A personal-assistant user works alone and occasionally shares a task with a relative. They have no company.
  • A learning-platform user might be an individual buyer or an employee whose company bought seats.
  • A workplace collaboration user is always inside a company.

Claude and ChatGPT model exactly this spectrum with two workspace kinds: every account has a personal space that exists from the moment you sign in, and you can additionally create or join team workspaces. We want the same shape so a single account can be a B2C user in one context and a team collaborator in another, with no separate "upgrade to org" signup.

The design questions are: (1) does a workspace exist before the user creates one, and (2) is "personal" just a team workspace with one member, or is it a distinct, constrained kind?

Decision

  1. Every user owns exactly one personal workspace, auto-provisioned. ListWorkspaces provisions the caller's PERSONAL workspace on first call. The user never creates it and never has a "no workspace" state.
  2. Personal workspaces are closed. A personal workspace admits exactly one member, the owner, and cannot be deleted. AddMember, CreateInvitation, and DeleteWorkspace are rejected for it.
  3. Team workspaces are the collaboration surface. CreateWorkspace makes a TEAM workspace the caller owns. A team is anything from a family to a whole company; it takes members at a role (owneradminmemberguest) and can be deleted by its owner.
  4. Personal and team are distinct kinds, not one with a member count. The WorkspaceType enum is explicit so the closed-ness of personal workspaces is enforced structurally. A team workspace that drops to one member is still a team workspace; a personal workspace can never become a team.

Because the end user is data, not the caller (ADR-0001), every management RPC carries an acting_user_id — the user the action is authorized as. It is required; omitting it returns InvalidArgument. The very first ListWorkspaces for a user auto-provisions their personal workspace:

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"}'
# => alice's PERSONAL workspace, provisioned on this first call.

Both kinds are the same workspace namespace object underneath: membership is mirrored as workspace:<id>#<role> tuples, so Check and resource inheritance (ADR-0001) treat them identically.

Consequences

  • Positive. One account spans B2C and B2B with no mode switch.
  • Positive. Auto-provisioning removes the "empty account" edge case — there is always an owner workspace for a user's first resource.
  • Positive. Closing the personal workspace makes a clear privacy guarantee: it can never accidentally gain a second member or be destroyed.
  • Negative. The personal/team asymmetry is special-cased in WorkspaceService (rejected mutations on personal workspaces).
  • Negative. A user who wants to collaborate must create a team workspace and move resources into it; there is no "promote my personal workspace to a team". This keeps personal workspaces private at the cost of a migration step — accepted, and mirrors how Claude/ChatGPT keep personal and team plans separate.