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/completionswith adst_key, setmodeltodst/<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.