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.
docker run brings the whole product online in about 90 seconds, wizard included.Principles
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.
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.
Versions are content-addressed blobs in object storage. Publishing never mutates; it appends. History and rollback are free, and the metadata DB stays small.
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.
The publish/comment/version API is a documented open spec. Any agent or tool can implement it. The product is one reference implementation.
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
*.dock.build TLSper-artifact subdomain routingdockMCP serverClaude Code / Codex skillweb uploaderplain HTTPProven 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
Biggest contributor pool for an OSS project, first-class MCP + AI tooling, trivial to read and extend.
DATABASE_URL (the n8n ladder)If "drop one static binary next to Postgres" is the louder enterprise ask, Go wins on self-host ergonomics.
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
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.
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.
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.
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 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.
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
| Table | Key columns | Notes |
|---|---|---|
artifact | id, short_id, org_id, slug, visibility, current_version | The stable identity behind a URL. short_id is what agents reference. |
version | id, artifact_id, n, blob_key, content_type, author, message | Immutable. blob_key points at the content-addressed object. Restore = set current_version. |
comment | id, artifact_id, base_version, anchor, thread_id, body, resolved | anchor is a DOM selector or MD line range. base_version ties feedback to what was seen. |
principal | id, org_id, email, kind (human/agent) | Humans and agents are both first-class authors. |
acl | artifact_id, visibility, password_hash?, org_gate? | public · link · org-sso · password. Defaults to private + noindex. |
The open standard
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.
| Op | Endpoint | Purpose |
|---|---|---|
| POST | /v1/artifacts | Publish a new artifact (multipart file + metadata). Returns short_id + URL. |
| POST | /v1/artifacts/:id/versions | Publish a new version against an existing artifact. |
| GET | /v1/artifacts/:id | Fetch content as Markdown/JSON for agent read-back. Supports @vN. |
| GET | /v1/artifacts/:id/comments | List open/resolved comment threads with anchors and base_version. |
| POST | /v1/artifacts/:id/comments/:cid/resolve | Resolve a thread (human or agent). |
The loop, end to end, from an agent's point of view:
Deployment
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.
Plan
@vN deep links, simple diff. Republish against a short_id.