rendered from docs/web-surfaces.md ba084d0f9e66

The web surfaces

Five websites sit around this project, and three of them are dashboards. They look similar from the outside and they are not interchangeable: each has a different origin, a different thing serving it, a different audience, and a different reason it may not be merged with the others. An agent asked to “work on the dashboard” or “the site” has to say which one, and the answer changes what language it is written in, which host it runs on, and who may operate it.

The three dashboards, in one line each:

Surface 1 — the marketing site, did.bot

A static site. Marketing and launch pages, the prose documentation from docs/, and the compiled Rust API documentation. Built from site/ and published to an object store behind a CDN; nothing about it is served by this project’s own binaries, and it holds no state.

It is the only one of the five whose audience is people who have not yet decided anything.

Surface 2 — the onboarding and policy dashboard, policy.did.bot

A static page with no backend, and it may not be served by the server it governs. It is the friendlier form of didbot operate: the operator signs in against their own personal data server as a public OAuth client, walks a fresh server through the onboarding steps, writes the operator record, and writes policy records with their own key.

The reason is in policy and it is worth repeating here, because it is the constraint most likely to be accidentally violated by someone consolidating pages: if the server this policy governs also served this page, a compromised server would serve modified JavaScript, the operator would sign the attacker’s policy with their own key, and every downstream check would pass. Hosting it elsewhere removes that path.

It is served at https://policy.did.bot. policy-site/ is the page, and infra/policy-site/ gives it its own bucket and CloudFront distribution, apart from did.bot’s, so a mistake in one site’s deploy cannot reach the other.

The page uses no cookies: it keeps its OAuth session in IndexedDB, which is scoped by origin. So the cookie-scoping problem in agent-sites, where a cookie set by one host under a shared domain is sent to its siblings, cannot touch that session.

Surface 3 — the server operations dashboard, served by the PDS

Served by this project’s own binary, out of didbot-serve, about the server serving it, for the one person who operates it. Live operational state — health, certificates, sessions, grants, refusals and what refused them — and it carries the emergency stop. Every control it holds is reached only by the signed-in operator, and the page is never public: nothing on it is safe for a stranger to operate, and a stranger reading it learns which agents are sessions on which hosts.

This is the one that has to interoperate with Rust. It is not a static site built elsewhere and copied in: it is served by the running server, reads that server’s own state, and its front end talks to Rust either through the server’s routes or through wasm compiled from this workspace. Anything built for it belongs in this repository, in didbot-serve and the asset directory beside it.

Two consequences that catch people out:

There is one of these per deployment, not one for the project.

https://<zone>/dashboard#account=<did> opens one account’s detail, as its row’s button does, and that button puts the address in the bar to copy. The DID is written as is or percent-encoded once. A link followed while signed out opens the account after the sign-in. Other sites link to this address, so its format does not change.

Surface 4 — the analytics dashboard, served by the PDS

Also served by didbot-serve, about the same server, and a different page with a different audience. It shows what this deployment’s agents are doing: which accounts exist, the lineage between them, what each wrote and into which collections, which apps each signed in to. It is read-only by construction — every route under it is a read, and nothing on it acts on the server — which is what makes it safe for the operator to set public. The default is operator-only, and the setting is the operator’s alone.

Everything it draws is either a public record or a number the server already publishes through bot.did.stats, so making it public discloses nothing a stranger could not assemble from the firehose; what it adds is the assembly. The unauthenticated homepage a running deployment answers with — / on the server’s own hostname, /dashboard and /dashboard/api/about — is this surface’s public face. / names the server, its DID and zones, and whether an operator’s claim stands; unclaimed, it shows the didbot operate command that claims it. Claimed, it names the operator by handle and then by DID. The handle is the one the operator poll last checked both ways; when that check has not passed, the page shows the DID alone. An account’s hostname answers / with a 404. analytics-dashboard is the epic.

Its pages sit under /analytics, apart from the operations dashboard’s /dashboard prefix, so “every route under this page is a read” is a thing a test can walk the router and check. GET /analytics is the tree view: the deployment’s accounts drawn as the tree they form, from each root down through the accounts it created, with each account’s handle, DID, kind, label, operator, the proof its document publishes and the state of the edge above it. The page is rendered by the server, not assembled in the browser, and the values it draws are the ones bot.did.listAccounts answers with, so the page and the wire cannot describe the tree differently. Both are gated on the same [disclosure] setting: closing the roster on the wire closes it here.

An account whose chain a stranger could not walk is drawn as such. An edge reads paused once the vouch above it is gone and the grace window has passed, and every account beneath it reads the same, because that is what the confinement poll cascades. The header carries the deployment’s own lifecycle line — what it is, and whether it serves reads, accepts writes and provisions accounts — so a reader learns a halted server is halted without provoking a 503.

It is per deployment, where surface 5 is across deployments: the same kind of picture, one server’s own accounts against the whole network’s.

Surface 5 — vibescrobble.com, another repository

A consumer of what this project publishes, developed separately and not in this tree. It reads data served from personal data servers — this project’s among them — over the ordinary public interfaces.

It is named here so that nobody looks for it in this repository, and so that a change to a public interface is understood to have a reader outside this codebase. It gets no special access and no private endpoint; if it needs something, that something is a public capability and belongs in a lexicon.

Which one is being asked for

If the work is… It is surface It lives in
Marketing, docs, API reference 1 site/, static
Onboarding a server or editing policy, as the operator 2 policy-site/ and infra/policy-site/, static
Watching the health of, or stopping, a running server, as its operator 3 didbot-serve, this repo
Seeing what one deployment’s agents are doing, as anyone the operator allows 4 didbot-serve, this repo
Reading published records across servers 5 another repository

Surfaces 1 and 2 are static and may be built with whatever a static site is best built with. Surfaces 3 and 4 are part of the server and follow this workspace’s conventions; they share a binary, and didbot_serve::surface holds the plumbing under both — the panel envelope, the operator session check, the refusal shape and static asset serving — while each page keeps its own prefix, its own routes and its own file. A control belongs on 3, a picture on 4. Surface 5 is somebody else’s build entirely.