Skip to content

Build your own UI

Everything in dst is an HTTP endpoint. The bundled dashboard is a pure client of the same public API you get — plain fetch with a bearer header (apps/web/src/api/client.ts), no privileged channel, no server-side tricks. Whatever it can do, your UI, script, or internal tool can do with the same calls.

Credentials

Two tokens, two planes (API reference):

Token Minted by Opens
dstadm_ admin token dst bootstrap (saved to .env as DST_ADMIN_TOKEN) /mgmt/* — manage lenses, connections, reviews, keys
dst_ caller key dst keys create --caller alex (or POST /mgmt/callers/{name}/keys) /v1/* — ask questions as a governed identity

Both ride the same header: Authorization: Bearer <token>. An asking surface should hold a caller key, not the admin token — the admin bypasses lens allow-lists, so access bugs stay invisible until someone else hits them.

The two calls a UI needs

What can this key see?

curl -s http://localhost:8000/v1/lenses \
  --header "Authorization: Bearer dst_..."

Ask:

curl -s http://localhost:8000/v1/lenses/customer_value/query \
  --header "Authorization: Bearer dst_..." \
  --header "Content-Type: application/json" \
  --data '{"q": "how many customers do we have?"}'

The response is an answer with receipts (Receipts) — render them, don't drop them:

  • answer — the prose; rows — the data.
  • sql, citations — what actually ran and why.
  • confidence (verified / partial / unverified) — derived from named checks; certification (certified / assisted / none) — whether a human approved this exact question→SQL pair.
  • data_as_of — the freshness stamp.
  • request_id — the handle for corrections (below).
  • clarification — when set, this is a governed non-answer, not an error: render the choices it carries and re-ask (Clarify & refusal).

If your UI phrases the reply itself, pass "format": "structured" — same SQL, same receipts, minus the LLM call that writes the prose, which is most of the latency — on certified answers, roughly two orders of magnitude.

Close the loop from your UI

A wrong answer reported from your surface feeds the same review queue as everyone else's:

curl -s http://localhost:8000/v1/reviews \
  --header "Authorization: Bearer dst_..." \
  --header "Content-Type: application/json" \
  --data '{"request_id": "<from the answer>"}'

GET /v1/reviews lists the tickets on this caller's own requests, so your UI can show reporters what happened (The correction loop).

Types without writing them

The server publishes its own contract — nearly every route declares a response model, so the schema is real, not decorative:

  • http://localhost:8000/docs — interactive, try-it-out, always current.
  • http://localhost:8000/openapi.json — generate a typed client:
npx openapi-typescript http://localhost:8000/openapi.json -o dst.d.ts

(or openapi-python-client, or any OpenAPI generator — nothing about the schema is TypeScript-specific.)

CORS for a separate origin

Served same-origin (dst serve mounts the dashboard bundle on the API's port), no CORS is involved. A UI on its own origin declares itself:

DST_CORS_ORIGINS=https://ui.example.com

Comma-separated for several origins. Dev servers on localhost:5173/3000 are allowed automatically outside production.

The UIs you don't have to build

  • Any OpenAI-compatible chat UI already works: point it at POST /v1/chat/completions with a dst_ key, set model to dst/<lens> — streaming included. A weekend chat template becomes a governed data UI in minutes.
  • Agent surfaces get the same powers over MCP — eleven tools, same governance (Agents over MCP).

The control plane is yours too

Every page of the bundled dashboard — lens editing, publish, connections, the review queue, observability — is /mgmt endpoints under the admin token, enumerated with descriptions at /docs. An admin UI, a CI check that polls /mgmt/observe/kpis, a bot that rules on review tickets: all the same API. A test pins every route to a docstring and the reference doc to the app, so what you read there is what is served.