Dock dock.build · v0.3
Technical brief

Boring tech, on purpose.

The constraint shapes everything: a large enterprise can stand this up in an afternoon and a solo dev can run it on a $5 box. That rules out anything exotic. One container with embedded SQLite + local blobs by default; Postgres and S3 when you scale; OIDC when you're ready. Nothing a platform team has not run a hundred times. The full install walkthrough lives in Execution.

0 external deps
Default mode: SQLite + local-disk blobs inside one container. Postgres and S3 are upgrades, not requirements.
0 command
One docker run brings the whole product online in about 90 seconds, wizard included.
0 tables
artifact, version, comment, principal, acl. The whole product fits in five.

Principles

Six rules that shape every decision

01

Standard parts only

SQLite/Postgres + disk/S3 behind two small interfaces, all in one container. No bespoke datastore, no queue we can avoid. If a Fortune 500 platform team has not run it before, we do not use it.

02

One artifact = one origin

Untrusted HTML is hostile by default. Every artifact is served from its own opaque subdomain and sandboxed, so a malicious script cannot touch another artifact or the dashboard.

03

Content is immutable

Versions are content-addressed blobs in object storage. Publishing never mutates; it appends. History and rollback are free, and the metadata DB stays small.

04

Same core, hosted or not

The hosted tier runs the exact open-source image. No "real" closed version. Enterprise features are config and a support license, not a separate codebase.

05

Protocol over product

The publish/comment/version API is a documented open spec. Any agent or tool can implement it. The product is one reference implementation.

06

Stateless app, scale flat

All state lives in Postgres + object storage. App containers hold nothing, so you scale from one VM to a fleet with no session affinity.

Architecture

The whole system on one screen

Edge
Reverse proxy (Caddy / NGINX)wildcard *.dock.build TLSper-artifact subdomain routing
App (container)
Node + TypeScript (Hono)REST + MCP serverrender & sandboxauth (OIDC / magic-link)TanStack Start UI embedded
Metadata
SQLite (default) → PostgreSQLartifacts · versions · comments · users · aclfull-text search (FTS5 / tsvector)
Blobs
local disk (default) → S3 APIAWS S3 · GCS · Azure · MinIO · R2content-addressed versions
Clients
CLI dockMCP serverClaude Code / Codex skillweb uploaderplain HTTP

Proven shape: Shopify's Quick serves 50,000+ sites off one small VM (object store + wildcard NGINX + identity proxy) for ~$200/mo, because compute is client-side. Dock uses the same physics and adds the metadata DB for versions and comments.

Language & the one real choice

TypeScript core, with a Go option on the table

Recommended: Node + TypeScript

Biggest contributor pool for an OSS project, first-class MCP + AI tooling, trivial to read and extend.

  • Hono HTTP (web-standard, zero lock-in) + official MCP SDK in one process
  • SQLite default → Neon Postgres via DATABASE_URL (the n8n ladder)
  • Huge ecosystem for sanitization & Markdown; biggest OSS contributor pool

Alternative: Go single binary

If "drop one static binary next to Postgres" is the louder enterprise ask, Go wins on self-host ergonomics.

  • One static binary, no Node runtime to vet
  • Lower memory footprint at idle
  • Smaller contributor pool, more MCP plumbing by hand

Decision

Start in TypeScript + Hono in one Node container for velocity and the agent ecosystem; keep the app stateless and the storage seams clean so a Go rewrite of the hot path stays possible if it's ever needed. Two precedents shape this: Shopify rewrote Quick's server Node → Go for memory/parallelism (our seams are the ones they needed), and Unkey built on Cloudflare Workers and had to rewrite to a stateful monolith — latency plus "self-hosting was nearly impossible." Container-first is their tuition, not our guess. Full stack table with receipts: Execution.

The hard part

Safely hosting untrusted HTML

Threat: an artifact is arbitrary attacker-controlled code

Anyone (or any agent) can publish HTML with scripts. Naively serving it from app.dock.build/abc would let that script read the viewer's session cookie, call our API as them, or phish on our domain. This is the single most important thing to get right.

Origin isolationper-artifact subdomain

Each artifact is served from its own opaque host, e.g. a8f3kx9.usercontent.dock.build, never the app domain. Same-origin policy then does the heavy lifting: artifact JS cannot reach the dashboard's cookies, storage, or API.

Sandbox + CSPdefense in depth

Embeds use a sandboxed iframe; the served document carries a strict Content-Security-Policy. Cookies for the artifact origin are SameSite and scoped so they are useless cross-site.

Auth at the proxygate before render

For gated artifacts, the reverse proxy checks the session before any bytes are served (the model Quick uses with an identity-aware proxy). Private content is never reachable unauthenticated, even at the object-store layer.

Markdown sanitizationrender path

Markdown is rendered server-side through a strict allowlist sanitizer, so MD artifacts cannot smuggle script. Raw HTML artifacts skip sanitization but are quarantined by the origin isolation above.

Resource limitsabuse control

Per-artifact size caps, per-org storage quotas, upload rate limits, and optional malware/secret scanning on publish. Abuse takedown is a single flag on the metadata row.

Data model

Five tables carry the whole product

TableKey columnsNotes
artifactid, short_id, org_id, slug, visibility, current_versionThe stable identity behind a URL. short_id is what agents reference.
versionid, artifact_id, n, blob_key, content_type, author, messageImmutable. blob_key points at the content-addressed object. Restore = set current_version.
commentid, artifact_id, base_version, anchor, thread_id, body, resolvedanchor is a DOM selector or MD line range. base_version ties feedback to what was seen.
principalid, org_id, email, kind (human/agent)Humans and agents are both first-class authors.
aclartifact_id, visibility, password_hash?, org_gate?public · link · org-sso · password. Defaults to private + noindex.

The open standard

Artifact Publish Protocol

The differentiator no closed vendor can copy: a small, documented protocol so any tool publishes and reads back the same way. The same operations exist as MCP tools so agents call them natively.

OpEndpointPurpose
POST/v1/artifactsPublish a new artifact (multipart file + metadata). Returns short_id + URL.
POST/v1/artifacts/:id/versionsPublish a new version against an existing artifact.
GET/v1/artifacts/:idFetch content as Markdown/JSON for agent read-back. Supports @vN.
GET/v1/artifacts/:id/commentsList open/resolved comment threads with anchors and base_version.
POST/v1/artifacts/:id/comments/:cid/resolveResolve a thread (human or agent).

The loop, end to end, from an agent's point of view:

# 1. agent publishes $ dock publish ./q1-review.html → https://acme.dock.build/8f3kx9-q1-review (short_id 8f3kx9, v1) # 2. human leaves 3 inline comments in the browser, signed in via SSO # 3. agent reads the artifact + open threads back (MCP or HTTP) $ dock comments 8f3kx9 --open --as md → [#1 @chart] "use median not mean" (base v1) → [#2 @intro] "drop the second paragraph" # 4. agent edits the file and republishes; same URL, new version $ dock publish ./q1-review.html --id 8f3kx9 --message "address review" → v2 live · comments #1,#2 auto-linked to base v1

Deployment

From laptop to enterprise, same image

Self-host in one command

# Mode 0: zero external dependencies $ docker run -d -p 8080:8080 \ -v dock-data:/data ghcr.io/niftory/dock ✓ sqlite + local blobs in /data ✓ setup wizard at :8080 (~60s)

Set DATABASE_URL / OBJECT_STORE_URL to graduate to Postgres + S3/GCS/R2; add OIDC_* for SSO. dock migrate --from-sqlite moves you with zero downtime. Full walkthrough: Execution.

Scaling tiers

  • Mode 0 — solo / small team: one container, embedded SQLite + disk. Handles thousands of artifacts; backup = snapshot one volume.
  • Mode 1 — company: N stateless replicas behind the proxy, managed Postgres, real object store, OIDC/SAML SSO.
  • Mode 2 — enterprise: Helm chart on their k8s, their VPC, their object store. Data never leaves. We sell support + SLA, not hosting.

Plan

Build order

M1
Publish + renderUpload HTML/MD, content-addressed blob, per-artifact sandboxed origin, permanent URL. CLI + web uploader. The "Netlify Drop for artifacts" core.
M2
VersionsImmutable version chain, restore, @vN deep links, simple diff. Republish against a short_id.
M3
Comments + agent loopInline anchored threads, resolve, and the MCP read-back that closes the loop. The moment Dock beats a static host.
M4
Auth & accessMagic-link (hosted) + OIDC (self-host), per-artifact public/link/org/password, proxy-level gating, audit log.
M5
Self-host polishOne-command compose, Helm chart, docs, the published Artifact Publish Protocol spec, hosted tier on the same image.
M6+
Optional backend APIsKey-value, AI proxy, realtime for richer interactive artifacts (the Quick playbook). Workspaces, search, analytics, embeds.