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.
The whole system, one screen
Requests enter at the router; writes serialize inside one vault's Durable Object; nothing crosses a tenant boundary.
Click a component
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.
Tenancy: the database is the tenant
Organizations own billing; vaults own knowledge; the isolation boundary is the vault.
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.
Tenant selection happens once
idFromName(orgId/vaultId) — after that, no query carries a tenant id. There is no cross-tenant WHERE clause to forget.
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.
One R2 key builder
r2Key() rejects empty/../slash segments; every object lives under orgs/<org>/vaults/<vault>/….
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.
Agents are principals
Keys carry principal_type: agent + a name. Every journal entry and every page's provenance names its author.
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.
Deletion is real
D1 soft-delete → storage.deleteAll() → R2 prefix purge → vector cleanup → key/link revocation → tombstone. Tested to zero residue.
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.
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.
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.
| table | holds | notable |
|---|---|---|
| pages | frontmatter + body, both layers | frontmatter_raw preserved → byte-clean round-trips of foreign YAML |
| index_lines | the master catalog | ≤120 chars, enforced; written in the same transaction as the page |
| log_entries | append-only journal | no UPDATE/DELETE statements exist in code — deletes append |
| links | wikilinks · md links · citations | the graph, re-derived per write |
| pages_fts | FTS5 virtual table | title×6 · desc×4 · headings×2 · body×1, privacy paths excluded at build |
| raw_files | source metadata + extracted text | bytes live in R2; ingest_status flips when a page cites the source |
| chunks | semantic bookkeeping | stable ids pageId:seq:hash; dirty-flagged on writes |
| privacy_rules · user_state · idempotency · schema_history | path-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.
Two engines, one trust policy
Semantic answers first and lexical is the announced fallback; both rank through the same group priors — the trust hierarchy as math. The console asks for the semantic engine on every interactive query and badges any degradation.
All model-touching stages sit behind ports (embedding, contextualize, rerank,
rewrite, vector index) with deterministic fakes — tests run the full pipeline shape with
zero cloud calls, and SEMANTIC_MODE selects real adapters in deployment. Local dev gets a
third mode: qmd routes the whole search to a supervised sidecar wrapping the local qmd
engine over a per-vault file mirror, so dev search is genuinely semantic with no remote bindings at all. The priors are the
point: the compiled, human-promoted wiki outranks raw notes at 1.4×; the archive sinks at 0.55×. Both
engines answer to the same policy.
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 withContent-Security-Policy: sandboxso 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
RateLimiterDurable Object token bucket applied to/v1/*withX-RateLimit-*+Retry-Afterheaders (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
siteverifyon the auth forms (CSP admits onlychallenges.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 —cancellingkeeps access untilcurrent_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.