Dock dock.build · v0.3

After v1 · the hard parts

Roadmap to world-class

v1 works end to end: publish, versions, diff, sticky comments, presence, analytics, auth, deploy. What separates a working product from a world-class one is a short list of genuinely hard problems. This page names them, makes the design calls, and sequences the work.

Phase A · Teams and trust

Everything else hangs off authorization. Build the spine first.

Organizations, members, roles A1

Better Auth's organization plugin (orgs, invites, member roles, active org in session). Artifacts already carry org_id; start stamping it on publish and scoping every read.

  • One authorization function in core: can(actor, action, artifact). Every entry point (web session, API token, MCP agent) resolves to the same actor shape and asks the same question. No parallel permission logic, ever.
  • Roles kept boring: owner / editor / commenter / viewer. Editors publish versions; commenters comment and resolve; viewers read.
  • Migration: each existing user gets a personal org; existing artifacts adopt their publisher's org. Self-host single-user keeps working with zero config (one implicit org).

Why it's hard: three principals (humans, static tokens, agents) hitting four surfaces (web, API, MCP, raw serving). The only defensible design is a single choke point. Retrofitting that later means auditing every route twice.

Scoped API tokens A2

Replace the single global DOCK_TOKEN with real tokens: hashed at rest, org-bound, scoped (publish, read, comment), optionally pinned to one artifact, with last-used tracking and revocation in settings. CLI and MCP use them natively.

  • DOCK_TOKEN survives as the self-host bootstrap secret only.
  • A CI token that can publish one artifact and nothing else is the unlock for "wire Dock into your pipeline" without fear.

Why it's hard: agents are the main API consumer, and agents leak credentials. Blast-radius containment (scope + pin + revoke) is the feature, not the hygiene.

Sharing that means something A3

The four visibilities exist in the schema; make them real. org enforces membership, password uses the existing acl hash with a clean unlock page, link stays unlisted (and gets noindex). A proper share dialog: visibility picker, copy link, copy at-version link, password set/rotate.

  • Guest comments on link artifacts become an org policy: name-only (today's behavior), or sign-in required.

Sandbox serving domain A4 security-critical

User HTML must never execute on the app's cookie origin. Hosted serving splits domains: app at dock.build, artifact bytes at a dedicated sandbox domain (the *.pages.dev pattern every serious host uses). The CSP sandbox stays as defense-in-depth; the origin split is the real wall.

Why it's hard: it touches cookies (auth never crosses), CORS, the viewer iframe, and self-host docs (one-domain self-host keeps the iframe sandbox as its isolation). Getting this wrong is how artifact hosts end up hosting credential-stealing pages that read the host's own session.

Phase B · The loop, industrialized

Publish → review → revise is the product. Make it the best place for humans and agents to close that loop.

dock init and the content standard B1

dock init scaffolds a publishable project: dock.json (artifact id, title, visibility, spa flag) so dock publish needs zero flags and always targets the same artifact. Plus the conventions that keep comments durable:

  • Markdown and HTML guidance that keeps text anchor-friendly (anchors survive edits when prose has stable context).
  • The anchor-client protocol (/raw/dock-client.js, the postMessage contract) documented as a public standard others can build viewers against.
  • An agents file in the scaffold: how to publish, read comments, reply in-thread, resolve with a republish. The agent loop as convention, not tribal knowledge.

Webhooks with an outbox B2 reliability

Per-org endpoints subscribed to comment.created, comment.resolved, version.published. HMAC-signed payloads, at-least-once delivery, exponential backoff, dead-letter after N attempts, and a visible delivery log with redrive.

  • Design: an outbox table written in the same transaction as the event, flushed by an in-process worker. No queue infrastructure; the SQLite-to-Postgres ladder keeps working.
  • This is what turns comments into an automation surface: comment fires a webhook, an agent reads the thread over MCP, publishes a fix, resolves. Nobody polls.

Why it's hard: delivery guarantees without new infra. The honest contract is at-least-once plus idempotency keys, stated loudly, with the failure path (dead-letter, redrive) in the UI instead of a log file.

Notifications that reach you: inbox, Slack, email B3 table stakes

Google-Docs-grade awareness: you should never miss a comment on your work. One notification router sits on the B2 outbox and fans each event to pluggable channels by org config and per-user preferences.

  • The parity matrix (who hears about what): @mention → the mentioned person. Reply → thread participants. New comment → artifact owner + watchers. Resolve → thread participants. Proposed version awaiting review (B5) → reviewers. Instant or daily digest, per user.
  • Arbitrary webhooks are the base primitive (that's B2): any event, HMAC-signed, to any URL. Everything else is a connector on the same router; if Dock never builds your tool's integration, the raw webhook already works.
  • Slack is the first first-party connector. v1 is an incoming-webhook URL per org/channel with rich formatted messages (artifact, version, quote, comment, deep link). Zero OAuth, self-host friendly; the same adapter shape covers Teams and Discord. In-app inbox (always on) and env-gated email ride alongside.
  • Slack v2: a real Slack app (hosted tier): per-user DMs for your mentions and threads, link unfurls of dock URLs into rich cards (pairs with the embeds work), and reply-from-Slack posting back into the thread later.

Why it's hard: notification systems die by noise. The router enforces one rule set (dedupe, self-action suppression, digest collapse) so adding a channel never re-implements the logic; channels stay dumb pipes.

An editor worth using B4

The textarea becomes CodeMirror: syntax highlight for md/html, live side-by-side preview through the real renderer, paste-an-image becomes an asset upload, and a commit-message field that makes version history read like a changelog.

  • Images in markdown make single-file artifacts grow assets: a file artifact quietly becomes a one-entry bundle with an assets/ dir. Same storage model, no new concepts.

Phase C · Serve like a platform

The hosted tier earns trust by serving fast and policing abuse.

Domain mode C1

Per-artifact origins: name.dockd.app subdomains, then custom domains. Host-header routing serves the artifact at /, which deletes the absolute-URL rewriting and gives every artifact a real origin (own localStorage, clean asset paths, SPAs that just work).

  • TLS: wildcard cert for the sandbox domain on hosted; Caddy on-demand TLS in the self-host docs.
  • The embedded /raw viewer stays for the comment loop; domain mode is the public face.

CDN-grade byte serving C2

Versioned paths are already immutable; finish the job: ETags, Range requests, streamed blobs (no full-buffer reads), and a documented CDN recipe (cache-everything on /raw/*/v/*, the app never in the byte path after first fetch).

Search C3

Full-text across titles and extracted text, indexed at publish. SQLite FTS5 and Postgres tsvector behind one MetaStore.search(): the same env ladder, no search service. Cmd-K in the app front-ends it.

Abuse, quotas, takedown C4 existential

Anyone hosting arbitrary HTML hosts phishing eventually. This kills artifact hosts that ignore it, so it ships before the hosted tier opens up:

  • Per-org quotas (artifacts, storage bytes, bandwidth) and rate limits on publish and comments.
  • A report endpoint and page on every public artifact; takedown state (removed) that 410s the content while preserving the record; audit log of moderation actions.
  • Link-visibility artifacts get noindex; the sandbox domain (A4) already keeps phishing off the app origin. Safe Browsing checks slot in later without redesign.

Why it's hard: it is not a feature users ask for, it has no demo, and skipping it works fine until the day it ends the product. Scheduled work, not best-effort.

Redis backplane C5

The EventBus interface already exists; add a Redis pub/sub implementation selected by REDIS_URL, and move presence to TTL'd Redis keys. SSE and presence then work across N stateless containers. Self-host single container keeps the zero-dep in-process bus.

The cool layer · concepts evaluated

Candidates for "what makes people show Dock to a friend", judged on value, fit with the loop, and cost. Verdicts are commitments, not vibes.

Proposed versions + review flow build highest value

Today a republish goes live instantly. Add a proposed state: anyone (especially an agent) can publish a candidate version; reviewers see proposed-vs-current as a diff, then approve (it becomes current) or request changes (a thread on the proposal). The pull-request model, applied to artifacts.

Why this wins: it completes the loop's missing half. Agents become safe contributors: they can propose all day, and a human gate decides what ships. No competitor in this space has review states on artifacts.

@agent in comments build

Mention a connected agent in a thread ("@claude tighten the executive summary"). The webhook (B2) fires with full thread context; the agent reads it over MCP, publishes a proposed version, and replies in-thread. Reviewer approves from the same screen.

Why this wins: it is the signature demo. Comment → agent revision → approve, without leaving the page. Rides entirely on B2 + proposed versions; the marginal cost is a mention parser and an agent registry per org.

dock dev · live watch mode build

dock dev ./report.md republishes on save, and every open viewer refreshes over the existing version.published SSE event. Reviewers literally watch an agent iterate, version by version.

Why this wins: demo magic at near-zero cost. The SSE channel and the republish path already exist; this is a file watcher and an iframe reload.

Pin comments · point anchors build

Text selection fails on charts, dashboards, and image-heavy pages. Add a second selector type to the anchor standard: a pin (element path + offset) dropped anywhere on the rendered page, shown as a numbered marker. Same threads, same re-anchoring honesty ("element changed" instead of silent drift).

Why this wins: AI output is increasingly visual. Figma-style pins on any HTML makes Dock the review surface for all of it, and the anchor client was built to carry a second selector type.

Live cursors · multiplayer presence build

Figma-style named cursors moving over the artifact while others view it, riding the presence we already have. A WebSocket lane in the same container (@hono/node-ws, no new service) carries throttled, ephemeral cursor positions; the dock-client grows a cursor capability (report the local pointer doc-relative, draw remote cursors inside the sandboxed iframe, where coordinates are honest). Selection-sharing rides the same channel later.

  • Self-host: works out of the box. One container does in-process fan-out; no Redis, no config. Self-host is the easy case, not the cut one.
  • Hosted scale: a Durable Object relay (the PartyKit pattern, partyserver): one DO per artifact, WebSocket Hibernation so idle connections cost nothing, a short-lived token minted by the API so authz stays with can(). Cursors are ephemeral pure fan-out, exactly what DOs are for, and it keeps the Node container stateless.
  • One wss:// URL either way: the client never knows which transport is behind it. Container WS ships first; the DO relay is a hosted-tier swap, not a rewrite. (No "Workers-first" violation: the core stays in the container; this is an edge accessory for one high-frequency lane.)

Why this wins: instant "this place is alive" feeling at low cost. The iframe coordinate wrinkle is the actual work, and the anchor client was built to carry exactly this kind of capability.

Embeds + unfurl cards build

OpenGraph preview images generated per artifact (title, version, comment count, thumbnail) plus oEmbed, so links unfurl rich in Slack, Notion, and Linear.

Why this wins: distribution. Every shared link becomes an ad for the loop. Cheap, and it compounds.

GitHub Action build

dock-publish action: CI publishes the build (docs site, coverage report, generated artifact) to a stable Dock URL with the comment loop attached. A thin wrapper over the CLI + scoped tokens (A2).

Collections maybe · AI change summaries maybe

Collections (group artifacts into a shareable hub page): useful, not differentiating; build when teams ask. AI change summaries ("what changed in v7", thread digests for stakeholders): genuinely valuable, but it puts an LLM dependency near core. If built: bring-your-own-key, env-gated, never required.

Public gallery pass · deck mode pass

Gallery/explore: a social surface is an abuse surface; nothing public-by-default before C4 exists. Slide/deck mode: renderer scope creep; HTML artifacts already do this.

Hard problems register

The five that deserve fear and respect, plus one deliberate cut.

  • Hostile HTML at scale (C4 + A4): the existential one. Origin isolation plus quotas plus takedown, shipped before growth, not after the first incident.
  • One authz model for humans, tokens, and agents (A1, A2): a single can() choke point or eventual security drift. There is no middle.
  • Origin isolation vs the embedded viewer (A4, C1): the comment loop needs the iframe; the public face needs real origins. Two serving modes, one storage model, kept honest.
  • Delivery guarantees without infra (B2): at-least-once webhooks from an outbox table, with the failure path in the UI. Resist the queue-service temptation until the ladder actually creaks.
  • Anchors under adversarial edits: re-anchoring is deterministic (context match, then exact, then "text changed"). The deliberate call: stay deterministic and honest rather than fuzzy-matching into wrong highlights. Comments degrade legibly, never silently misattach.
  • cut Realtime co-editing (CRDTs): explicitly out of scope. Dock's model is versions + comments, not Google Docs. The version is the unit of review; this cut keeps the whole system comprehensible.

Sequence

Each step ships as its own PR train, browser-verified like everything so far.

A1 orgs/roles → A2 scoped tokens → A4 sandbox domain → A3 share dialog
B1 dock init + standard (+ dock dev watch mode) → B2 webhooks
B5 proposed versions + review flow → B6 @agent in comments → live cursorsB3 inbox + Slack connectorB4 editor (+ pins)
C4 abuse/quotas → C1 domain mode → C2 CDN serving (+ embeds/unfurls, GitHub Action) → C3 search → C5 backplane
hosted-tier launch sits after C4: never open signups before the abuse story exists.
the value crown is B5+B6 (propose → review → approve, with agents as contributors); A1/A2/B2 exist to make it land fast.