Design documents and hosting architecture
Canonical Octonode repository documentation.
Design documents and hosting architecture
Status: implemented foundation (2026-07-31) Owners: schema, cloud control plane, Studio
Product contract
Design Docs is a collaborative document surface for technical decisions. A document may belong to the active workspace or to one project, is edited as Markdown, HTML, or plain text, and is previewed as a paged document. Reviewers can pin revision-anchored comments at document coordinates. Authors can embed workflow references or sandboxed HTTPS iframes, publish a revocable unique path as either an open file or a hosted page, and use the browser print pipeline to save the hosted view as PDF.
This is separate from repository documentation. Files under a project remain source-controlled and canonical in the project store. Design Docs is collaboration data: immutable content revisions live in R2 and metadata lives in the workspace social authority. The two surfaces may link to each other, but neither silently copies or overwrites the other.
User surfaces
| Surface | Scope | Purpose |
|---|---|---|
Studio /studio/design-docs | Workspace | Documents shared independently of one project |
| Project Design docs tab | Project | Decisions and references for one project |
/design/:unique-path | Public capability path | Hosted page or open-file view, according to access mode |
@octonode/design-docs | Standalone package | Storage keys, embed grammar, safe URL rules, and share paths reusable by another app |
The Studio implementation is deliberately a consumer of the package and generated API. A future standalone app can import the same package and call the same HTTP contract without importing Studio, the workflow editor, or server internals.
System shape
flowchart LR
Author[Studio author] --> API[Generated design-doc API]
Reviewer[Workspace reviewer] --> API
Public[Unique public path] --> PublicAPI[Public read-only route]
API --> Worker[Cloud control Worker]
PublicAPI --> Worker
Worker --> Social[Workspace Social Durable Object]
Worker --> R2[(DESIGN_DOCS R2)]
Social --> Meta[(Document metadata + access)]
Social --> Comments[(Existing design review comments)]
Preview[Document renderer] --> Embed[Sandboxed iframe / workflow reference]The Worker is the only R2 reader/writer. The bucket is private. A public URL never maps directly to an R2 key: the Worker resolves the workspace and document from the unique slug, asks the workspace Durable Object whether the slug is current and public, and only then reads the referenced object.
Persistence and consistency
Durable Object SQL
design_documents owns scope, access, the current share slug, optimistic version, and update time.
Each document also owns a backing design_artifacts row. This lets document comments reuse the
existing immutable design target, review thread, reply, resolve, moderation, realtime, backup, and
restore machinery rather than creating another comment platform.
design_document_revisions stores title, format, SHA-256, UTF-8 byte size, R2 object key, and the
immutable document revision. Its (document_id, document_revision) foreign key points to the
backing design artifact revision, so every coordinate pin names the exact bytes that were reviewed.
Workspace documents use @workspace only as their internal comment-project key. That sentinel is
not a project authorization shortcut; routes authorize them at workspace scope. Project documents
carry the owned project id and repeat the project permission check after resolving document metadata.
R2
Content is stored under:
design-docs/{workspace-kind}/{encoded-workspace-id}/{document-id}/{revision}.{md|html|txt}Objects are immutable because each save creates a new UUID revision and key. The Worker writes the object first, commits metadata second, and deletes the new object if the metadata transaction fails. Idempotent replays delete an unused duplicate object. Old revisions remain available for anchored comments and recovery; retention cleanup must not delete an object still referenced by metadata.
The DESIGN_DOCS bucket is intentionally separate from plugin bundles, avatars, and social backup
archives. Cloudflare's Workers R2 API provides the required private binding, streaming reads, object
metadata, and explicit delete operation; no public r2.dev bucket is required.
Revisions and conflicts
Create and save requests require an Idempotency-Key. Save also sends baseRevision. If another
writer has appended a revision, the API returns 409 with the current revision; it never overwrites
the winning content. Share mutations use the document's integer version for the same reason.
Comments store normalized x/y coordinates and the viewport that produced them. Text comments also store the selected quote plus short prefix/suffix context, while keeping the coordinate as the visible pin. A pin therefore survives responsive rendering of the same revision. Changing document bytes creates a new revision; old pins remain on the old revision and are never silently moved. A later projection feature may offer a best-effort mapping, using the existing detached-anchor state when confidence is insufficient.
Access and unique paths
| Access | Authenticated Studio | Unique path |
|---|---|---|
private | Creator and moderator only | Disabled and revoked |
workspace | Members with design:read; edits need design:review | Disabled and revoked |
public_file | Same authoring rules | Opens Markdown or plain-text source; HTML source is served as text, never active same-origin markup |
public_host | Same authoring rules | Opens the standalone document renderer |
Switching to a public mode generates a new unguessable slug. Changing or revoking public access invalidates the previous path because the Durable Object stores only the current slug. Public reads return document bytes only. Comments, authoring mutations, member identities, and historical revisions remain authenticated.
The public API path is the only dynamic exception to server API authentication and supports GET
only. Cloud Worker routing resolves it before user authentication but still validates the exact
slug against the owning Durable Object. Responses use nosniff; raw HTML receives a plain-text
content type so a shared document cannot execute on the Studio origin.
Rendering and embeds
The portable embed grammar is intentionally small:
{{workflow:project-id/workflow-id}}
{{iframe:https://reference.example/path}}Studio inserts workflow references from the current project's workflow selector. For Markdown it
also captures the current topology in a portable fenced Mermaid block; preview projects the live
reference to an iframe. HTML and text use the same directive, so embeds do not depend on unsafe raw
HTML. Ordinary Markdown/HTML is sanitized before insertion.
Raw <iframe>, scripts, styles, objects, and srcdoc are removed.
Embed URLs must resolve to HTTPS, except same-origin HTTP during local development. Every iframe has
sandbox="allow-scripts allow-forms", lazy loading, no referrer, no same-origin privilege, no popup,
download, or top-navigation permission. Workflow embeds use a Studio deep link and still enforce the
workflow's own authentication and project permissions. Publishing a document never publishes an
embedded private workflow.
This first slice embeds the live workflow view. A future immutable workflow snapshot should be added only when public offline workflow rendering is required; it must store a schema-owned graph snapshot on the document revision rather than granting a public workflow token.
Comments and realtime
Selecting preview text records its quote/context and a normalized position. Add page comment
switches the preview into a one-click coordinate mode for visual areas that have no text. Submitting
creates an ordinary design review thread targeting the backing artifact and exact document revision.
Every local review comment snapshots the authenticated user or agent id and display label, plus its
avatar reference, into the existing comment identity columns. The recorded id/label preserves who
made the review; the avatar reference follows the user's current profile image.
Replies and resolve operations use the existing review endpoints with complete design target context.
The workspace social socket broadcasts design.updated; React Query reloads open design-document
thread queries so collaborators see new replies and resolution changes without polling.
Anonymous public viewers cannot read or create comments. This keeps workspace discussion and member identity private even when the document itself is public. If public commenting becomes a requirement, it needs a separate abuse, identity, moderation, notification, retention, and rate-limit design.
Save as PDF invokes the browser's native print dialog. Print CSS removes Studio chrome, the ruler, comment pins, background, borders, and shadows while preserving document layout and embeds as the browser renders them. This is the smallest reliable cross-platform export and works in both Studio preview and the standalone hosted page.
A server-generated binary PDF is intentionally not part of this slice. Add Cloudflare Browser Rendering only when automated, pixel-stable, headless exports or attachment delivery are required; at that point it needs queued jobs, bounded navigation, font policy, iframe timeout behavior, R2 PDF retention, and a signed download route.
API
| Method | Path | Result |
|---|---|---|
GET | /api/design-documents?projectId= | List workspace or project documents |
POST | /api/design-documents | Create document and first immutable R2 revision |
GET | /api/design-documents/:id | Read metadata and current content |
PUT | /api/design-documents/:id | Append a revision using baseRevision |
POST | /api/design-documents/:id/share | Set access and rotate/revoke unique path |
GET/POST | /api/design-documents/:id/review-threads | List or pin revision comments |
GET | /api/design-documents/shared/:slug | Public hosted payload |
GET | /api/design-documents/shared/:slug?raw=1 | Public raw file payload |
All authenticated Studio calls use generated client code through api/design-documents.ts and
React Query hooks. The public hosted page uses the generated public GET operation.
Limits and security checklist
- Content is limited to 2 MiB measured as UTF-8 bytes at both server and Worker boundaries.
- Titles, ids, formats, scopes, coordinates, viewports, comment bodies, and access values are schema bounded.
- R2 keys are server generated; clients never supply object keys.
- Private reads are creator/moderator-only in the Durable Object, not merely hidden in UI.
- Project documents repeat the project-scoped permission check after metadata resolution.
- Public routes are exact GET paths; mutation and prefix lookalikes remain protected.
- Sanitization occurs after Markdown conversion and strips active raw HTML.
- Iframes receive restrictive sandbox and referrer policies.
- Unique paths are revocable and never authorize comments or workflow access.
- R2 failure yields
503; metadata never claims missing bytes as a successful read.
Delivery tasks
- T192 — architecture and contracts: document authority, scopes, access, embeds, PDF, and standalone boundary.
- T193 — portable package and schema: add
@octonode/design-docs, Zod wire contracts, key/embed helpers, and checks. - T194 — R2 and metadata: add the dedicated binding, schema v4 migration, immutable revisions, rollback, and public resolution.
- T195 — generated HTTP API: add authenticated CRUD/share/comment routes plus the exact public read route and regenerate.
- T196 — Studio and standalone UI: add workspace/project navigation, canvas, editor, hosted page, access control, and stories.
- T197 — embeds, comments, and PDF: add workflow/iframe insertion, coordinate pins, replies/resolve, and print stylesheet.
- T198 — release verification: generation, build, focused tests, Storybook build, and affected visual screenshot review.
- T199 — identified live review: snapshot commenter identity, add text-range anchors and one-click page pins, and refresh threads from realtime review events.
Follow-on work
- Add revision history selection and detached-anchor projection when reviewers need to compare versions.
- Add immutable workflow snapshots when a public document must show a workflow without granting workflow access.
- Add automated binary PDF jobs only when browser print cannot satisfy an actual delivery workflow.
- Add public comments only with an approved anonymous identity and abuse-control model.