docs / 02 · architecture
chapter 02 · design & architecture

Isolation you can point at.

The decisive design call: physical per-tenant isolation over logical discipline. Each vault is one Durable Object with its own SQLite database — there is no WHERE tenant_id to forget, because the database is the tenant. Everything else follows from that.

interactive · click any box

The whole system, one screen

Requests enter at the router; writes serialize inside one vault's Durable Object; nothing crosses a tenant boundary.

helios-router one domain · VMFE www/ · Astro docs/docs · Astro blog/blog · Astro app · console/app · Hono SSR api/api write plane mcp/mcp agents VaultDO × None per vault · SQLitepages · index · log · FTS OrgDOquotas · registry D1control plane R2raw files · backups Queue → helios-ingestembeddings · workflows Vectorizenamespace/vault Workers AIbge-m3 · rerank · compile
start here

Click a component

the map is the document

Every box is a real deployed unit with a real config file. Click one to see what runs there, what it may touch, and which invariant keeps it honest.

01

Tenancy: the database is the tenant

Organizations own billing; vaults own knowledge; the isolation boundary is the vault.

ORGANIZATION — billing boundary · D1 row · one OrgDO vault A · VaultDO own SQLite · own R2 prefix own vector namespace grants: owner·editor·agent·viewer vault B · VaultDO assistive or agent-operated capabilities discovered, not assumed vault C — created on demand, destroyed with zero residue
Fig. 1 Org → members → vaults. Agent API keys are first-class principals scoped to one vault — the journal can always say which agent wrote a page.

The seven invariants

Frozen tests pin every one of these; the matrices are data files a harness iterates, with positive controls so a broken route can't fake a pass.

I·1

Tenant selection happens once

idFromName(orgId/vaultId) — after that, no query carries a tenant id. There is no cross-tenant WHERE clause to forget.

I·2

Authorization before the stub

getVaultStub(env, access) takes a VaultAccess produced only by the auth middleware. It is the single idFromName call site for vaults.

I·3

One R2 key builder

r2Key() rejects empty/../slash segments; every object lives under orgs/<org>/vaults/<vault>/….

I·4

Vectors carry no text

Vectorize is partitioned by namespace = vaultId (platform-enforced — verified live). Metadata is IDs only; hydration goes back through the owning VaultDO.

I·5

Agents are principals

Keys carry principal_type: agent + a name. Every journal entry and every page's provenance names its author.

I·6

Share links can't write

A share token maps to a read-only surface with no mutation routes registered at all — and it does not authenticate on the normal API.

I·7

Deletion is real

D1 soft-delete → storage.deleteAll() → R2 prefix purge → vector cleanup → key/link revocation → tombstone. Tested to zero residue.

02

The edge: vertical microfrontends

One domain, six routed verticals plus the async ingest worker — each ships independently; the user never sees the seams.

The router longest-prefix-matches the path, strips the prefix, and forwards over a service binding. On the way back, HTML from prefixed verticals passes through HTMLRewriter: root-absolute href/src/action get the prefix re-applied, a view-transition stylesheet is injected so cross-worker navigation feels like one app, and speculation rules preload the /app vertical. JSON, streams, and the /api + /mcp verticals pass through byte-identical. Forwarding is transport-hardened: the request is cloned before the first attempt and retried exactly once on a thrown transport error — so POST bodies survive a dropped service-binding hop — and a persistent failure returns a branded 502 naming the vertical instead of an unhandled exception. Cross-vertical links in page chrome survive the rewrite via a segment-aware preserve list (/pricing, /changelog, /terms, /policy + every vertical mount), while upstream Location headers are re-prefixed with upstream-namespace semantics — a vertical’s own redirect to /x means its /x, so trailing-slash redirects can no longer escape their vertical. In production the router is the only Worker with public routes: custom domains for usehelios.co, with www 301-redirected to the apex.

GET /docs/x the user's URL strip → /x binding: DOCS vertical replies src="/logo.png" rewrite + inject src="/docs/logo.png"
Fig. 2 The reverse-proxy illusion. Each vertical also works standalone on its own workers.dev URL — the prefix exists only at the router.

Auth crosses the seams because everything shares one origin at the router: Better Auth lives in the app vertical, cookies are host-only on the router hostname, and the API worker independently validates every request — session cookie or hk_… API key. The router adds no auth headers and the API trusts none.

The stack is intentionally mixed: the interactive console remains Hono SSR, while the reading verticals — marketing, docs, and blog — are Astro 7 static assets-only Workers. They share the editorial token layer in packages/design/tokens.css and self-host Newsreader, Geist, and Geist Mono from packages/design/fonts/; each static Worker carries its CSP in _headers. Astro's base paths pair with the router's prefix stripping so /docs/_astro/* and /blog/_astro/* survive both local and deployed routing.

03

Inside a VaultDO

The pages table is the truth; every derived structure is disposable and rebuildable from it. Files round-trip byte-clean out the export door.

tableholdsnotable
pagesfrontmatter + body, both layersfrontmatter_raw preserved → byte-clean round-trips of foreign YAML
index_linesthe master catalog≤120 chars, enforced; written in the same transaction as the page
log_entriesappend-only journalno UPDATE/DELETE statements exist in code — deletes append
linkswikilinks · md links · citationsthe graph, re-derived per write
pages_ftsFTS5 virtual tabletitle×6 · desc×4 · headings×2 · body×1, privacy paths excluded at build
raw_filessource metadata + extracted textbytes live in R2; ingest_status flips when a page cites the source
chunkssemantic bookkeepingstable ids pageId:seq:hash; dirty-flagged on writes
privacy_rules · user_state · idempotency · schema_historypath-pattern privacy (human-only edits) · per-user UI state · 24h replay · versioned migrations run just-in-time in the constructor

Slow work never blocks the object: each vault schedules its own maintenance through the single alarm slot (a tiny task scheduler persists per-task next-runs and always arms the minimum) — nightly validate + backup-tar to R2, and a 60-second write-debounced re-embedding batch pushed to the ingest queue. No global cron scans tenants; every vault wakes itself.

05

Security model

Markdown is hostile input now. The local system trusted its own notes; a SaaS cannot.

  • Rendering: markdown → allowlist sanitizer built on HTMLRewriter (only markdown-producible elements survive; per-element attribute allowlists; scheme checks kill javascript:/data:/<base>/SVG vectors) under a strict CSP. Raw sources are served with Content-Security-Policy: sandbox so a malicious clipped page can't script against the app.
  • Secrets never land: a scanner (key shapes + entropy heuristics) rejects writes containing credentials — record the sanitized principle, never the secret, as a machine rule.
  • Rate & quota: per-principal token buckets in the OrgDO, plus an org-scoped RateLimiter Durable Object token bucket applied to /v1/* with X-RateLimit-* + Retry-After headers (fail-open, reset on plan change); plan limits enforced at single choke points and rendered on the billing page from the same constants — marketing can't drift from enforcement.
  • Auth surface: better-auth with email/password + Google/GitHub OAuth, env-gated email verification, no-enumeration password reset, Turnstile server-side siteverify on the auth forms (CSP admits only challenges.cloudflare.com), a fixed-window limiter on auth POSTs, and 15-minute cookie caching on 7-day sessions.
  • Subscription lifecycle: Polar webhooks are idempotent (claim-table runOnce + stale-event guard); cancellation is a grace period — cancelling keeps access until current_period_ends, a daily expiration workflow is the missed-webhook safety net, and sign-up fans out welcome + day-3/7/14 onboarding reminder workflows with deterministic instance ids.
  • Telemetry never reads content: the structured logger refuses fields named body/content/text/frontmatter; metering datapoints carry ids, operations, latencies.
  • The viewer plane cannot write: the console holds no mutation surface for vault content — promotion and settings are its only writes, both human-gated.