Flow
ServicesWorkspace Service

Getting started

Run the workspace service locally, authenticate as a dev principal, and walk a project from creation to context pack.

Prerequisites

Follow Local development, then:

./localenv up workspace-api

Workspace calls two other services. Both are optional, and each fails differently when it is not running. It is worth starting without them once, on purpose, to see what that looks like:

UpstreamStarted withAbsent means
documents./localenv up documents-apiFile routes return 503; everything else works
intelligence./localenv up intelligence-apiThe pack returns knowledge.degraded: true

Local values come from services/workspace/process-compose.yaml; every key is documented in services/workspace/.env.example.

Who you are locally

When deployed, the caller's identity comes from a Cloudflare Access token, or from a broker credential the person minted for themselves. That token's email is the same principal as the Access session. Locally, DEV_AUTH=1 takes it from a header instead. The default is [email protected]. Set DEV_ACTOR in the repo-root .env to use a different address; ./localenv passes it through. That person is a platform admin: DEV_AUTH grants those powers to whoever the header names, so a local checkout does not have to keep WORKSPACE_PLATFORM_ADMINS in sync with DEV_ACTOR. A service token is still only as powerful as the person it names on the configured list.

curl -s localhost:8104/v1/projects -H 'X-Dev-Principal: [email protected]' | jq

The service refuses to start with this turned on unless APP_ENV is local or dev. Setting one variable in a deployed environment is not enough to enable it.

Walk a project

Create it

curl -s localhost:8104/v1/projects \
  -H 'X-Dev-Principal: [email protected]' \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'Content-Type: application/json' \
  -d '{"data":{"name":"Q3 pricing","description":"Rework the tiering",
               "kind":"project","visibility":"company"},
       "why":{"code":"manual.start","detail":"kicking off pricing work"}}' | jq

Every mutation is a Command: An HTTP POST that changes state, recorded with an Idempotency-Key so retries do not double-apply; not CQRS or a command bus.: it needs an Idempotency-Key: A caller-supplied header, stable across retries, that lets command.Execute claim the operation once and replay the same response on duplicate requests. and a why, and the response is wrapped in the Response envelope: The template service wraps every JSON response in {header, payload}. A success payload is {data} (a record or a list) plus optional meta.pagination; accounting still returns bare bodies and RFC 9457 errors until the divergence is resolved.. Keep the returned id.

Talk in it

curl -s localhost:8104/v1/conversations \
  -H 'X-Dev-Principal: [email protected]' \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'Content-Type: application/json' \
  -d '{"data":{"project_id":"'"$PROJECT"'","title":"Tier boundaries"}}' | jq

curl -s localhost:8104/v1/conversations/$CONVERSATION/turns \
  -H 'X-Dev-Principal: [email protected]' \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'Content-Type: application/json' \
  -d '{"data":{"role":"user","content":"We agreed to drop the middle tier."}}' | jq

Turns can only be added, never edited, and they are numbered in order with no gaps. That is why the feed is paged with a cursor rather than a page number: paging by number would re-read everything before your position on every poll, and that grows all day.

Read the pack

curl -s localhost:8104/v1/projects/$PROJECT/pack \
  -H 'X-Dev-Principal: [email protected]' | jq

With intelligence running, the knowledge section fills in once the conversation has been read. That takes a moment. Intelligence waits for a conversation to go quiet before reading it, so an active conversation costs one pass instead of one per turn. Without intelligence running, you get the project sections on their own and knowledge.degraded is true.

Watch the events

Every write also writes a row to its domain's outbox, in the same transaction, and the API sends those out. Run ./localenv up workspace-conversationlistener to watch the conversation events arrive. If nothing is moving, the local app shows how deep each queue is.

Tests

go test ./services/workspace/...

They need Postgres and nothing else. The documents and intelligence clients are Go interfaces, and the tests pass in fakes.

On this page