HIP-1121: Dataroom — Documents Shared by Link
Abstract
/v1/dataroom is a secure document room: upload documents, group them into
rooms, share them by access-controlled link, and watch who read what, page by
page. It is implemented in hanzoai/cloud at apps/dataroom, which runs the
ported Papermark business logic in-process rather than as a separate service.
This HIP states where the bytes live, where the rows live, and how an
unauthenticated viewer is routed to exactly one tenant.
Motivation
The upstream product was a Next.js + Prisma + Postgres deployment — a pod, a
database and a framework for what is, to the cloud, one subsystem. The fold
(apps/dataroom/dataroom.go:4-9) retires that deployment: cloud serves the
surface itself, on the same per-tenant storage every folded application uses,
and the standalone pod holds nothing to migrate.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 Two stores, one owner
Rows — documents, rooms, links, viewers, page-view events — live in one SQLite
file per tenant, opened by the shared goja host with one transaction per
request (apps/dataroom/dataroom.go:12-20). Document bytes never touch the
bundle or local disk: they go through the object-storage seam (deps.VFS)
under an org-scoped opaque key, and the tenant DB persists only the key
(apps/dataroom/dataroom.go:30-35). Beside the tenant files sits one
system-namespace index, link_index (apps/dataroom/index.go:26) — the link
id → org routing table, the single deliberately cross-tenant piece.
§2 The addresses
Every route is under /v1/dataroom (manifest/apps.go:336). Ten routes are
typed operations. Seven stay raw, each for a reason in the wire
(apps/dataroom/dataroom.go:159-163): the upload takes the file itself as the
raw body, the two /file routes answer a byte stream, and the four
/view/{linkId} routes carry no validated org a typed op could read — they are
the visitor's surface. Each raw route declares its prose beside the wire fact
(apps/dataroom/dataroom.go:212-236).
§3 Tenancy
Admin routes require a validated principal and resolve the org from it
(apps/dataroom/dataroom.go:344-346, HIP-0026); refusal is principal.Refused,
never the unscoped store. Viewer routes carry no principal: the link id is
resolved through the link index to the owning org before any per-tenant store
opens, and a link with a password checks it through a bcrypt host function.
Isolation is therefore a host property — the bundle is handed a database already
pinned to one tenant and cannot name another.
§4 Money, events, telemetry
dataroom is free, in those words (plugin/dataroom/main.go:21,
cloud.Free; not in spend.go:275). It publishes no events on the bus —
page views are rows in the tenant DB read back through the analytics routes,
not bus events — and it emits nothing to observability beyond the request span
every route gets.
§5 Stage
dataroom is beta: a vertical application, not the agentic-OS core. The
manifest row declares it (manifest/apps.go:336, Stage: Beta), so the
capability is reached by flag (HIP-0139 §8).
§6 Upstream
dataroom embeds github.com/hanzoai/dataroom v1.1.7 — the ESM-free port of the
Papermark API handlers, pinned and checksummed rather than copied in. The
upstream is Papermark, AGPL-3.0 outside its ee/ directories (the module's
LICENSE carries the split), and what survives in HEAD is the domain logic as a
goja bundle: documents, rooms, links, viewers, analytics. The Go leaf adds only
the tenant schema, the object-storage seam, the bcrypt host function and the
link index; zero domain logic lives in Go (apps/dataroom/dataroom.go:17-20).
Rationale
The alternative to the link index is scanning tenant files for a link id, which turns every anonymous view into a walk of every tenant's store — slow, and a cross-tenant read performed on every request instead of never. One small routing table that maps id → org keeps the cross-tenant surface to a single lookup whose answer is an org name, not data.
Security Considerations
The viewer path is an unauthenticated endpoint into tenant data by design, so the whole exposure concentrates in the link: a guessable id is a readable dataroom. Link ids are crypto-random, a passworded link verifies through bcrypt before any page is served, and the index answers only the owning org — never rows. The other exposure is the bytes: they are keyed by org-scoped opaque keys on the object store, so a copy of one tenant's SQLite file contains no document content, only keys the store will not honour for another caller.
References
- HIP-0026 — Identity and Access Management
- HIP-0106 — Hanzo Plugin Contract
- HIP-0139 — Capability
Copyright
Released under CC0 1.0 Universal Public Domain Dedication.