Before you begin
Have built Studio files, a running store configured with Lys and a Lys app for the door. Register the door’s HTTPS callback with that app. Put a TLS proxy in front of the loopback door listener. The app credential stays in a file readable by the door user.
Studio source lives in surface/studio. Its build script checks TypeScript and builds assets into dist. These pages used the existing built assets for route checks. Rebuilding Studio and proving its current source in a browser were not part of the documentation check.
Serve the door
Write the door file with the public origin, built files, store sockets and Lys details.
{
"listen": "127.0.0.1:7741",
"origin": "https://studio.example.au",
"files": "/opt/haematite/studio",
"stores": {
"wards": "/run/haematite/ward.sock"
},
"lys": {
"issuer": "https://lys.example.au",
"client_id": "haematite-studio",
"secret_file": "/etc/haematite/lys-app-credential",
"redirect": "https://studio.example.au/callback"
}
}
haematite door --config /etc/haematite/door.json
Open the public origin. Studio first asks /api/haem for the configured stores. Without a session it sends the browser to /signin. Lys signs the person in and returns to /callback. The door validates the browser-bound state, exchanges the code and verifies the returned pass for audience haematite.
The session cookie is HttpOnly, Secure and SameSite=Strict. Short-lived sign-in cookies use SameSite=Lax so the callback can return from Lys. The pass and refresh token stay server-side. A door restart loses its in-memory sessions, so people sign in again.
What the door serves
GET /api/session answers the holder, expiry and refresh status used by Configuration. GET /api/haem lists configured store names. Listing a socket does not grant access to its contents.
POST /api/haem/wards takes a method and params, connects to that store, presents the session’s pass and carries one verb. The request’s Origin must exactly match the configured Studio origin. GET /api/haem/wards/follow?branch=main follows head changes as server-sent events. It needs a session, and any supplied Origin must match.
POST /signout forgets the server session and clears the browser cookie. It requires the Studio origin and answers 204 even if the session was already gone. It does not revoke the person’s other Lys sessions.
Other GET addresses serve built assets or the application. Hashed assets can be cached indefinitely and served with gzip. Keep a complete matching asset set when replacing Studio files.
Choose the place you are reading
Studio uses hash routes. A link such as #/data/patients?store=wards&branch=main names the store and branch. Add version=generation.sequence inside the hash query to hold a retained version. Without a version the tab follows the branch head.
An unknown named store or branch is refused. Studio does not silently open another one. A named version is checked against retained history before the screen reads it. Browser preferences choose the initial place only when the address leaves it out.
Read and change records
Home, #/home, shows collections, branch counts, store counts and recent versions. A count the service cannot supply is shown with its reason rather than invented.
Tabular, #/data or #/data/patients, reads collections in pages. Open a row to inspect its fields and values. A record address is #/r/patients/p0; record tabs keep their own address. Forms live at #/f/patients/p0, or omit the id for a new record. Writes carry the version read, so a changed record is refused instead of overwritten.
Objects, #/objects, defines collection schemas, fields, relationships and record pages. Select an object at #/objects/patients. Its page builder is #/objects/patients/pages/name; its templates are under #/objects/patients/templates. Drawing, #/model, is the Objects tab for the collection model. Templates and actions gives their stored declarations and limits.
Query and search
Query, #/query, runs checked pipeline or jq text. Read the marked refusal span when a word, argument or stage placement is wrong. Results are bounded pages at a captured version. Inspect the result origin before following it to a record.
Analytics, #/analytics, keeps named questions, their parameters and runs. Query modules, #/modules, is its tab for reusable query definitions. A missing or unreadable saved definition is shown by name. Search, #/search, reads text matches and links them to records. Saved query and view data lives in the store; browser preferences do not replace it.
Vectors, #/vectors, lists vector fields, their coverage and the record or text question used for nearest records. A supplied-vector field can use a stored vector question. A text question needs a declared embedded field, matching model and ready worker. The search reference explains those distinctions.
Relations and views
Graph, #/graph, reads relationships and records with bounded graph pages. A collection and record may be named as #/graph/patients/p0. Filters, selected types and the query are kept in the graph address. A WebGPU or painting failure is visible; a partial read is not a complete graph. See Relations and the graph.
Views, #/views, holds named views and their arrangements. #/p/name opens a saved page. A view is scoped to a store and branch. A refused or pending save retains its visible state and names the problem; it does not mean that another browser can read the draft.
Branches and history
Branches, #/branches, lists branches, their versions and review controls. Read all review pages and pass that captured source version to merge. A merge refuses a moved source or target. Changes, #/activity, is the Branches tab for comparing retained moments. A reversed comparison shows what would be undone, with additions and removals exchanged.
Use a branch for a change that needs review. Import and pack actions now generate names with a readable stem and a unique suffix, without /. Object editing uses a stable name with a digest of the full object name. The new-branch form accepts 1 to 512 ASCII letters, digits, underscores, hyphens or dots. The service still decides whether a name is placeable; a refused name is branch_name_unplaceable. This describes the source, rather than a browser run of the changed screens.
Configuration
Preferences, #/settings, controls browser choices, read sizes, value drawing and kept work. It also shows the Lys session and offers sign-out. A value above the service maximum remains a preference but reads are limited by the served maximum.
Stores, #/stores, lists the door’s named stores and their protocol, counts and sizes where available. It cannot add a socket or change service.json. An administrator changes those files and restarts the relevant process.
Import and export, #/io, reads CSV, JSON and NDJSON for import onto a branch and exports a collection at one captured version. A cancelled or failed export reports partial progress. A collection export omits the rest of the store and is not a backup.
Packs, #/packs, lists saved groups of collection schemas. Adding a pack attempts its changes on a branch for review. A missing pack collection gives an empty list, not an installed pack.
The navigation palette, keyboard help and movable utility panels are available across screens. The Assistant panel (⌘I) talks to your store’s assistant through the door. From 0.21.0 the door serves the assistant’s routes under /api/assistant/<store> when its configuration names an assistant.socket; without one the panel says the door carries no assistant. Use the assistant covers the panel.
Read a refusal on screen
session_unknown or session_expired means sign in again. sign_in_state_unknown or sign_in_state_refused means begin a fresh sign-in in this browser. busy_pending_signins means the pending-state bound is full; complete or restart abandoned sign-ins before trying again.
origin_refused means the proxy or browser used the wrong origin. door_not_reached means the browser could not complete the door request. store_unknown means this door has no configured socket by that name. permission_refused names a missing Lys grant. version_conflict or record_changed means read the current record before deciding how to apply the edit.
already_run means the action has a run record for that record version. not_offered means the record no longer meets its condition. action_log_failed means its run history could not be recorded; read the refusal and current record before repeating the action.
The refusals reference includes the remaining named Studio, transport, storage and worker diagnostics. The diagnostic reference retains library source locations. The scratch door verified files, session routes, callback, forwarding, origin checks and sign-out. The screen descriptions here are source-derived from App.tsx, railItems.ts and the screen modules, rather than a claim that every screen was exercised.