One product core, surrounded by replaceable service seams
Architecture
- The app owns product APIs. Artifact persistence, ACLs, publishing, rendering, live collaboration, MCP operations, and the SPA all converge in
services/app. - Contracts and utils unify cross-service libraries. The same Actor, SQL, Browser, Part, and Upstream interfaces work in one process or across HTTP.
- The proxy is a policy edge. It resolves identity, applies rate-limit doors, hosts login/OAuth, sanitizes forwarding headers, then delegates semantic decisions to the app.
- Documents are compiled products. Stored JSX is parsed, validated, enriched, server-rendered, hydrated in a sandbox, and connected to SQL/data/live-update transports.
Client / agent / unfurler
│ HTTP
▼
┌──────────────────────────────┐
│ services/proxy │
│ identity · doors · OAuth │
│ forwarding-header ownership │
└──────────────┬───────────────┘
│ Actor + Request
▼
┌──────────────────────────────┐
│ services/app │
│ API · ACL · storage · SPA │
│ documents · live · MCP │
└───────┬──────────────┬───────┘
│ SqlService │ BrowserService
▼ ▼
services/sql services/browser
DuckDB queries Chromium capture
Shared beneath every edge:
services/contracts ← types + constants
services/utils ← assembly + transports + signing| Workspace | Owns | Must not own |
|---|---|---|
contracts | Actor, Part, route, DB, SQL and browser types | Runtime behavior or dependencies |
utils | Assembly, transports, signing, env and service helpers | Product semantics |
proxy | Identity, rate limits, login, OAuth, forwarding | Artifacts or duplicated API routes |
app | Product behavior and all user-facing surfaces | Choosing local versus remote heavy services |
sql | Bound, capped, interruptible DuckDB execution | Persistent product storage |
browser | URL-to-image Chromium capture | Artifact authorization |
Takeaway: package boundaries follow authority, not merely technical layers.
2. Every request crosses one identity seam and one app boundary
services/proxy/src/parts.ts is an ordered declaration of middleware ownership. assemble() turns named parts into a Hono app and permits controlled replacement by name. The last part forwards unmatched traffic through an Upstream, either directly to app.fetch or over signed HTTP.
session
bearer → account session → agent cookie → anonymous
│
rateLimit
route → named door → IP/actor bucket → 429 or continue
│
loginRoutes / oauthRoutes
proxy-owned identity endpoints
│
forwardedHeaders
discard caller identity claims; establish public origin
│
forward
inProcess(request, actor) OR overHttp(signed actor header)
│
createAppServer()
actorReceiver → generated API routes → document routing → SPAInside the app, server/routes.generated.ts is derived from filesystem route modules under app/**/route.ts. server/app.ts adds static assets, request-scoped context, uniform 404 behavior, canonical URL healing, the reader-versus-owner document split, and bootstrapped SPA page data.
Takeaway: trace identity bugs from proxy session resolution to actorOf(request); trace product verdicts from the generated route into app libraries.
3. The app core revolves around artifacts, wire operations, and storage
lib/artifacts.ts is the persistence and authorization center: create/read/update/delete, ownership, versions, revert, shares, dependencies, ref resolution, and dataflow preparation. Route handlers stay thin by delegating request-body semantics to lib/artifact-wire.ts and transport-neutral agent actions to lib/operations/registry.ts.
| Core module | Relationship |
|---|---|
db.ts + schema.ts | PGLite/Postgres adapter and additive boot-time DDL |
artifacts.ts | Calls DB/object stores; enforces artifact lifecycle and ACL |
artifact-wire.ts | Turns API bodies into shared artifact pipelines and wire responses |
operations/registry.ts | One curated surface rendered as MCP tools, bearer HTTP, and docs |
tokens.ts, viewer.ts, share-roles.ts | Credential ownership, effective role, read/edit/annotate permissions |
object-store/* | Local or S3 bytes for images, datasets, and imported fonts |
annotations.ts | Pinned discussion threads attached to stable markup anchors |
profiles.ts, shelf.ts, urls.ts | Discovery, organization, public profiles, canonical and pretty paths |
POST /api/artifacts or MCP create_artifact
→ operation / route translation
→ artifact-wire validates one content tier
→ markup: parse + validate + refs + CSS + dry-run data checks
dataset: coerce + infer columns + store rows
image: fetch/decode + optimise + store bytes
viz: validate recipe + bindings
→ artifacts persists head + metadata
→ response returns id, URL, edit_id, versionTakeaway: new product semantics belong in the shared app pipeline first, then thin transports expose them.
4. Stored JSX becomes a sandboxed, live, data-aware document
The document subsystem is a compiler/runtime split. lib/jsx parses, validates, and serializes static JSX. lib/story extracts Helmet declarations, references, dataflow, titles, images, mutation/query contracts, and document assembly inputs. lib/story-ui interprets validated nodes against the component registry. lib/story-runtime hydrates the result and coordinates data, editing, annotations, slides, and live replacement.
stored source │ ├─ lib/jsx: parse → AST → validate → canonical serialize │ ├─ lib/story: Helmet + refs + flow + CSP + compiled Tailwind │ ├─ lib/story-ui: AST interpreter → React component registry │ ├─ lib/story/document: standalone HTML + SSR + data island │ └─ lib/story-runtime: hydrate + mx API + live/data/edit transports Reader URL /a/:id ├─ plain reader + simple markup → document served top-level └─ owner/editor/commenter/live-data → SPA shell + sandboxed frame
| Subtree | Change it when… |
|---|---|
lib/jsx | Syntax, allowed tags/URLs, validation, or round-tripping changes |
components/kit + story-ui/registry | A document-facing component or icon is added |
lib/story | Publish/serve semantics, data declarations, refs, CSP, or edit composition changes |
lib/story-runtime | Hydration, live updates, editing, comments, slides, or window.mx changes |
lib/viz + components/viz | Vega preparation, themes, maps, facets, or tooltip behavior changes |
lib/story-surface | Typography and viewport behavior shared by rendered documents changes |
Takeaway: maintain parser, interpreter, SSR, and hydrated runtime as one contract; a document must mean the same thing at every stage.
5. Data and live updates move independently from document structure
Documents carry declarations and final geometry before query rows arrive. The runtime store resolves referenced datasets, binds scalar values, and calls query endpoints. lib/sql/run-dataflow.ts orders query dependencies and delegates execution through the registered SqlService. This keeps page paint independent of DuckDB latency while preserving a capture exception that waits for complete content.
DATA
<Value>/<Query> declarations → runtime store → /query
→ resolve readable datasets → runDataflow → SqlService → rows
LIVE
DB wakeup → /events emits {editId, version, by}
→ client fetches /events/frame
→ cached complete structure replaces frame
→ changed flow signature triggers fresh queries
EXPORT
/a/:id/export → ACL → exportImageResponse
→ short-lived signed capture URL → BrowserService
→ PNG/JPEG bytes; card mode gets version-busted cache URLThe service registry in lib/services.ts is the inversion point. Composition roots register local implementations; SQL__SERVICE_URL and BROWSER__SERVICE_URL select HTTP clients. Callers deep inside publishing, mutation, dataflow, and export never choose deployment topology.
Takeaway: preserve the transport-neutral service contracts when extending queries or exports; topology should remain a deployment concern.
6. The SPA is product chrome; documents remain isolated content
web/ contains the Vite SPA router, shell, and pages. components/ contains app chrome and owner workflows; components/kit/ is deliberately the separate vocabulary rendered inside documents. Page-data endpoints under /api/page/* also feed server-inlined bootstrap JSON, so the first SPA paint has final geometry.
| Surface | Primary owners |
|---|---|
| Human app pages | web/App.tsx, web/Shell.tsx, web/pages/*, components/* |
| Agent API and MCP | app/api/artifacts/*, app/mcp/route.ts, operations/registry.ts |
| Agent documentation | skills/artifact-bin/*, lib/skills/*, app/docs/* |
| Shared document | app/a/[id]/raw, story compiler/runtime, per-document CSP |
| Owner/editor view | SPA artifact page, frame, edit/comment/live transports |
| Static/runtime assets | public/, generated story runtime, themes, fonts, geo assets |
Takeaway: app UI and document UI may share React, but they have different security, styling, and lifecycle boundaries.
7. Start changes at the narrowest source of truth
- 1. Name the authority. Decide whether the change belongs to identity/doors, artifact semantics, document meaning, UI chrome, SQL execution, or capture.
- 2. Change the shared seam first. Extend a contract only when behavior crosses a process boundary; otherwise keep the change inside the owning package.
- 3. Preserve transport parity. Agent actions should flow through the operation registry and shared artifact pipelines so HTTP, MCP, and docs stay aligned.
- 4. Preserve render parity. Document features must agree across validation, serialization, SSR, hydration, live frames, and export capture.
- 5. Test at the nearest level, then cross the seam. Use API/node/UI Vitest projects for local contracts and browser gates for behavior only a real server, stream, CSP, or browser can expose.
| If changing… | Begin with… | Verify across… |
|---|---|---|
| Auth or quotas | proxy/parts, contracts doors, viewer roles | proxy → actor → app ACL |
| Artifact CRUD | artifact-wire + artifacts | HTTP + MCP + versions |
| Document syntax/component | jsx + registry | publish → SSR → hydrate → edit |
| Data/chart behavior | story/dataflow, sql, viz | query → runtime → capture |
| Live collaboration | events routes + frame + runtime store | ACL → SSE ping → frame adoption |
| Deployment topology | contracts/utils + composition roots | local and HTTP conformance |
Takeaway: the shortest safe path is source of truth → shared seam → thin adapters → cross-boundary verification.