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- API: localhost:8104
- Swagger UI: localhost:8104/api-docs
- Database:
postgres://postgres:postgres@localhost:5433/workspace
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:
| Upstream | Started with | Absent means |
|---|---|---|
| documents | ./localenv up documents-api | File routes return 503; everything else works |
| intelligence | ./localenv up intelligence-api | The 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]' | jqThe 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"}}' | jqEvery 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."}}' | jqTurns 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]' | jqWith 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.
Workspace Service
Projects, conversations, notes, meetings, files, and notifications — the service agent-desktop talks to, plus the one endpoint that mixes in what the company knows.
Architecture
The eight workspace domains, why they never join, how another service reads the text, and what redaction actually destroys.