Hanzo

Dataroom

Package dataroom is a secure document room you share by link and watch page by page.

Package dataroom is a secure document room you share by link and watch page by page.

Base URLhttps://api.hanzo.ai
Operations17
AuthAuthorization: Bearer $HANZO_API_KEY

Specification

HIP-1121 · Dataroom — Documents Shared by Link — Draft · read the specification →

/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 door 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.

Four surfaces

SurfaceReaches this capability asCoverage
RESTdataroom at its own prefix17 operations
CLIhanzo dataroom …17 of 17
SDKDataroomApi in every published client17 methods
MCPtool dataroom on https://api.hanzo.ai/v1/mcp17 operations, 1 under the document's own id — ask describe for the rest

Quickstart

export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys

Then the first call — a read that needs nothing but the key. GET /v1/dataroom/links, operation get_dataroom_links:

hanzo dataroom links get

Answers 200 with object — ok.

Endpoints

EndpointWhat it does
GET /v1/dataroom/analytics/dataroom/{dataroomId}Rolls up every share link pointing at one data room: session and page-view totals for the room, plus the per-page breakdown for each link beneath it.
GET /v1/dataroom/analytics/link/{linkId}Reports how one share link was actually read: total viewing sessions, total page views, and per page the view count, the summed dwell measure and its…
POST /v1/dataroom/datarooms/{id}/documentsPuts an already-uploaded document into one of the caller org's data rooms and answers with the new membership id.
GET /v1/dataroom/datarooms/{id}Reads one of the caller org's data rooms together with every document in it, each carrying its membership id and order index.
GET /v1/dataroom/dataroomsReturns every data room in the caller org's own store, newest first, with its short public id, name, description and timestamps.
POST /v1/dataroom/dataroomsOpens a new data room for the caller org and answers with it, including the short public id it is addressed by.
GET /v1/dataroom/documents/{id}/fileDownload a document's bytes as its owner
GET /v1/dataroom/documents/{id}Reads one of the caller org's documents — its name, opaque storage key, content type, page count, size and timestamps.
GET /v1/dataroom/documentsReturns every document in the caller org's own store, newest first — name, opaque storage key, content type, page count, size and timestamps.
POST /v1/dataroom/documentsUpload a document's bytes and record it
GET /v1/dataroom/healthLiveness of the dataroom subsystem
GET /v1/dataroom/linksReturns every live share link in the caller org's own store, newest first, with the controls a visitor will meet: whether an address is required,…
POST /v1/dataroom/linksGrants access: it mints a public share link over one data room (dataroomId) or one document (documentId) — one of the two is required — and…
POST /v1/dataroom/view/{linkId}/authenticatePass a share link's gates and open a viewing session
GET /v1/dataroom/view/{linkId}/document/{documentId}/fileRead a document's bytes as an authorised link visitor
POST /v1/dataroom/view/{linkId}/pageviewRecord one page-view against an open viewing session
GET /v1/dataroom/view/{linkId}What a share link's visitor sees before authenticating

All Hanzo APIs · Interactive reference

How is this guide?

On this page