Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Web UI

drsg serve serves a single-page dashboard, embedded in the binary, for inspecting and operating a database from a browser. It uses the same JSON-RPC API as the SDKs over HTTP, and a WebSocket for live updates. Being embedded, it makes no external network requests: all assets are served from the same origin, and all rendering is local.

Access and authentication

Open the address reported by drsg serve (default http://127.0.0.1:7700). On a loopback address, the UI needs no setup: with no token configured, the same-origin check authorizes it; with DRSG_TOKEN set, the server writes the token into the page it serves to a loopback browser (as a <meta> element the app reads — never as a script), and the UI presents it as a bearer credential. Cross-origin requests are refused regardless.

Served on any other address (--addr 0.0.0.0:7700, the container image), the server requires a token to start at all and never writes it into the page, because that page goes to anyone who can reach the port. The first request the server answers unauthorized opens a prompt; paste DRSG_TOKEN there. It is kept in the tab’s session storage (gone when the tab closes) and sent as Authorization: Bearer on every call and as ?token= on the WebSocket, the only form a browser socket can carry. The dashboard’s own origin must also be listed in allowed_origins / DRSG_ALLOWED_ORIGINS there, since it is not loopback.

A loopback bind behind a reverse proxy on the same machine (or a forwarded port) is a network deployment, not a local one: every client on the internet reaches the server from a loopback peer address. The server treats it as such wherever it can tell — it never writes the token into a page whose request a proxy forwarded (Forwarded, X-Forwarded-*, X-Real-IP, Via) or addressed to a name other than this machine (Host), and listing the proxy’s origin in allowed_origins, which the dashboard needs to work behind one, turns injection off for every page and makes a token mandatory at startup (an allowed origin off loopback never counts as the local UI either). A proxy that adds no header and keeps a loopback Host cannot be told from a local browser, so for that shape set DRSG_PAGE_TOKEN=0 ([server] page_token = false), which serves the page bare regardless; the dashboard then asks for the token as it does on any other address. Set a token whenever anything stands in front of the listener.

Every response carries a Content-Security-Policy under which scripts and styles load only from the server itself; the dashboard is built to satisfy it, and a reverse proxy in front should pass it through rather than replace it.

A wrong token may be presented five times; after that the server answers that client’s requests carrying a token 429 Too Many Requests with a Retry-After that doubles per further failure, up to five minutes, until a correct token is presented. Requests with no token are neither counted nor blocked, so a tokenless local dashboard never trips it and the page and /health keep answering for everyone else. The client is the connection’s address; behind a reverse proxy on the same machine it is the address the proxy names in X-Forwarded-For, so one guesser does not lock out everyone the proxy serves. A JSON-RPC batch holds at most 64 requests.

Wherever the interface asks for a provider — semantic search, natural-language queries, AIgest — it offers the presets (openai, deepseek, qwen, ollama), and the API accepts exactly those, or the one provider the server was configured with ([server] embed_provider). A base URL is refused over the wire, because the server would be calling it from its own network on behalf of whoever holds a credential; an operator who wants a local or self-hosted endpoint configures it on the server. Errors that would describe the server’s own disk or its provider’s reply come back as a category and a reference (storage error (ref 00002a)); the detail is in the server’s log under that reference.

The interface has three views, selected from the header: Dashboard, Explore, and AIgest.

Dashboard

The Dashboard presents database health and plane management.

  • Health. A grid of statistics — planes, nodes, edges, labels, edge types, declared indexes, average degree, commits (the commit sequence), and on-disk size. These are pushed live over the WebSocket, so they update as the database changes; a connection indicator shows the live/offline state.
  • Plane cards. One card per plane, showing its name, description, and node/edge counts. A card selects the plane (the app-wide context), exports it as JSONL (a download the CLI’s import reads back), or deletes it behind a type-to-confirm dialog. A “New plane” card creates one.
  • Provenance. A card states where its plane came from and what it may ask. A plane served from a mounted database carries the mount’s name; a plane that attaches others (plane.attach) carries one tag per attachment, those on another instance marked remote with the URL in the tooltip. A plane that is neither mounted nor attaching anything shows no such row.
  • Mounted databases. When this process serves other stores alongside its own (db.mount), a section below the plane cards lists each one: its name, the path it was opened from, how many planes it contributes, and whether those planes carry the mount’s name as a prefix — which they do only when two mounts offered the same plane name. Each plane is a button that selects it. Nothing is copied and nothing is re-digested; the planes keep their own names and appear among the cards above. The section states the directory mounts are confined to, and is absent when nothing is mounted.

Explore

Explore is an interactive graph canvas driven by a tabbed toolbar. Selecting a node or edge opens an inspector showing its labels/type and properties (vectors are collapsed behind a control rather than printed); double-clicking a node expands its neighborhood; dragging from one node to another opens the new-edge dialog with the endpoints prefilled.

Reading a dense graph

A plane of any size is more than a canvas can show at once, so Explore draws the skeleton and lets you ask for the rest.

  • Ranked seeding. The opening view is the 40 most connected nodes and the edges among them, not the first 40 the scan reaches — a legible shape rather than an arbitrary sample. Show more widens it to 100, 200, 500. Ranking is by degree; graph.seed also offers PageRank, but PageRank pools rank in sinks, so a hub can score below its own neighbours.
  • The legend is a filter. Click a label to hide that category, click again to bring it back. Hidden, not faded: a category you switched off is not part of the question.
  • Crowded hubs fold. When a node has more than twenty leaves — neighbours attached to it and to nothing else — they collapse into one bead labelled with the count. Click the bead to open it. Nothing is discarded, and a leaf you have selected is never folded away.
  • Fans are grouped. Below that threshold, a hub’s leaves are arranged around it in arcs by label, so a ring reads as regions rather than a smear.
  • Importance opens the layout. Edges touching a well-connected node are laid out longer, so a busy neighbourhood has room to be read.
  • A large layout runs off the main thread. Past 400 nodes — Show all on a real plane — the force layout runs in a web worker for a bounded few seconds while the page stays responsive; the picture converges in view and settles when the run ends.
  • Expand one hop is batched. Growing the frontier asks for at most 300 nodes a click, in JSON-RPC batches of 64, and says how many were left for a second click and how many calls failed.
  • Selection focuses. The selection and its immediate neighbours stay at full strength, the next ring dims, and everything beyond recedes and drops its label. Selecting an edge focuses both of its endpoints, since an edge is a statement about two things.

Node size tracks how connected a node is (counting anything folded beneath it), and the legend maps colors to labels.

The toolbar tabs:

  • Filters / Operations — seed the canvas from a label (or the whole plane).
  • Algorithms — run PageRank, connected components, shortest path, or Louvain and overlay the result on the current graph (scores as node size/color, components/communities as color groups).
  • Hybrid — fused vector + keyword + graph-proximity search, with the channels and label selected in the bar.
  • Ask — a natural-language question; the generated plan is shown (with a copy control) and its connected result is plotted.
  • Time-travel — a slider over commit history (see below).
  • Live — the change feed (see below).

Query

A view of its own, beside Dashboard and Explore, and the one place a query is written: Explore draws the graph, this view asks it questions and reads the answers (Chapter 4).

The editor is several lines, since a query worth a page is rarely one: **⌘/Ctrl

  • Enter** runs it, plain Enter is a newline, and Tab accepts a keyword completion ghost-hint as you type. Example queries sit under the editor as starting points, and the text survives a reload.

The result takes the shape the query asked for:

  • a projection (RETURN n.file, count(*) AS n) comes back as a table, with its row and column counts, the time it took, and a copy control that puts the whole thing on the clipboard as tab-separated text — header included, so it pastes into a spreadsheet;
  • a query returning whole records (RETURN n) lists them by key, labels and the properties that fit;
  • a write reports its change counts.

A query that cannot compile shows the parser’s own message, which names the construct rather than a character position.

The header carries a quick search over the current plane, in two modes:

  • Text — substring matching across keys, labels, and string properties.
  • Semantic — embedding-similarity ranking (with an embedding-provider selector).

Selecting a result focuses that node or edge in Explore. The search also respects the time-travel cursor: with a past commit pinned, it searches the graph as it was at that commit.

Time-travel

On a native-backend server, Explore’s Time-travel tab probes the queryable window (plane.history) and, when available, presents a slider over the commit sequence with a Live control at the latest end. Dragging back re-plots the graph as of that commit; the seed and node expansions read the historical snapshot. A marker on the canvas indicates the pinned commit on every tab, and returns the view to live when dismissed. The header search reflects the same cursor. On a non-native backend the tab is absent.

The slider spans the retained window, not every commit ever made: plane.history starts at the floor [server] retain_commits keeps (20 when the key is omitted; set retain_commits = 0 for unbounded history), the readout says how many commits that is, and db.stats reports the setting as retain_commits. Raise it before you need the depth — versions past the floor are reclaimed at compaction and cannot be reached afterwards.

Wires

An HTTP route is the one reference a parser cannot turn into an edge: the handler and the caller are often in different languages, in different repositories, and between them there is only a string. So the preprocessor plugins record what each side states — serves on a handler, requests on a caller, both as VERB /path/{} with every variable segment reduced — and the match is computed when someone asks rather than stored.

Explore’s Wires tab reads those statements for the current plane and pairs them. Reading the routes runs the same selection the wire verb does — every node stating either side, and nothing else — so a plane of any size answers with its routes rather than with itself.

  • Matched lists each request beside every declaration that answers it. A request nothing answers is listed too, marked and tinted rather than hidden: it is the more interesting half, and usually means the handler lives in a repository this graph does not hold.
  • Requests and Serves list one side on its own, each route shown both normalized and as written, so /api/orders/{} can be read back against the /api/orders/{id} the source spells.
  • A verb filter and a path-substring filter narrow all three, and every symbol is a link that centers that node on the canvas with its neighborhood.

Two sides match on verb and path: the paths must be equal, and the verbs equal or either one * — a side that names no verb matches every verb on that path. Paths compare exactly, because the plugins normalized them when the graph was built; a route recorded by no plugin, or written in a form other than VERB path, is skipped rather than guessed at.

Live feed

Explore’s Live tab opens a plane.watch subscription over the WebSocket and streams commits as they land, newest first: each entry shows the operation (created / updated / deleted, color-coded), the kind (node / edge), the key or id, the labels, and the commit sequence. The stream can be paused and resumed, and narrowed to one label; selecting a node change focuses it in the canvas.

AIgest

The AIgest view ingests a document into the current plane (see Chapter 3). Upload or paste text — Markdown and plain text, or any of Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV and PDF, which are converted to Markdown so the model sees headings, tables and lists rather than loose characters — choose the chat and embedding providers, and Preview: the model extracts the entities and relations and proposes them. Write to graph commits the previewed proposal with no further model call. Options include linking to existing nodes (to avoid duplicates) and skipping embeddings.

A URL row sits beside the upload control: paste an address, optionally a topic, and Fetch. What comes back is not text in the box but a list — the page and the pages it links to, each with its relevance score, title and size, ticked if it cleared the floor. Unticking one removes it from the document with no further request, and a fold-out shows what was not kept and why. Nothing becomes tokens until the list looks right (Chapter 3).

Mode selects how thoroughly the extraction is cleaned up — coarse, fine (the default), or super — and is remembered like the provider choices, since it is a standing preference rather than a per-run decision. Choosing super raises a notice directly under the control: it re-reads every entity against all of its passages, at roughly 15× the input token usage. See Chapter 3 for what each mode buys.

Design

The dashboard is theme-aware (it follows the viewer’s light/dark preference) and fully self-contained: because it is embedded and same-origin, it depends on no external CDN, font, or service, and issues no requests beyond the API and WebSocket of the server that served it.