Cloudsteading
Product image still needed. This listing has source documentation, but no reviewed screenshot yet.

Open Artifacts

Publish versioned HTML and Markdown artifacts from a Cloudflare Worker.

Open Artifacts is a self-hosted Claude Artifacts alternative built on Cloudflare (D1, R2, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.

Source & license

Upstream license: MIT

License TL;DR

You can use it, change it, self-host it and sell it. Keep the original copyright and license notice with copies of the code. You don’t have to publish your changes. The authors don’t promise it will work.

Explain MIT in plain English →

Summary of the main license. Separate packages and assets can have different terms.

Inspect repository ↗Read this project’s actual license ↗

Repository owner

@coda0HQ

See the upstream repository for the original creator and contributors.

Maintain this project? Maintainer verification →

Cloudflare hosting

Free tier eligible within limits

The documented Open Artifacts deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs.

Hosting requirements
  • Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits.
  • Keep aggregate D1 use below 5 million rows read/day, 100,000 rows written/day and 5 GB total storage; a request can touch many rows.
  • Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account.
  • Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations.
Check current pricing ↗
Sources checked 01/10/2026

Repository snapshot: 4f8b54b. Hosting eligibility reflects the deployment documentation and listed assumptions.

  • claude-artifacts ↗

    Open-source, self-hosted [Claude Code Artifacts](https://code.claude.com/docs/en/artifacts): let any coding agent publish self-contained HTML/Markdown pages to shareable URLs, protect them with passwords (zero-knowledge, client-side encryption), and keep them updated as the project they describe evolves. Runs entirely on Cloudflare (Workers + D1 + R2), fits in the free tier, no accounts anywhere.

  • workers ↗

    { "$schema": "./node_modules/wrangler/config-schema.json", "name": "open-artifacts", "main": "src/index.ts", "compatibility_date": "2026-07-03", "workers_dev": true, "assets": { "directory": "./public", "binding": "ASSETS", "run_worker_first": ["/", "/api/*", "/a/*", "/og/*", "/fonts/*", "/vendor/*"] }, // "1" opts this deploy into the web-font surface: the /fonts proxy, // allowlisted font CDNs, and an allow-sam

  • d1 ↗

    the viewer stays unchanged. "vars": { "OPEN_ARTIFACTS_WEB_FONTS": "1", "OPEN_ARTIFACTS_HANDOFF": "1" }, "d1_databases": [ { "binding": "DB", "database_name": "open-artifacts", "database_id": "a01d21cd-86cb-4832-872d-d196e6918a3b" } ], "r2_buckets": [ { "binding": "CONTENT", "bucket_name": "open-artifacts-content" } ], "rules": [ { "type": "Text", "globs": ["**/*.txt"], "fallthrough": true }, { "type": "CompiledWasm", "globs": ["**/*

  • r2 ↗

    "database_name": "open-artifacts", "database_id": "a01d21cd-86cb-4832-872d-d196e6918a3b" } ], "r2_buckets": [ { "binding": "CONTENT", "bucket_name": "open-artifacts-content" } ], "rules": [ { "type": "Text", "globs": ["**/*.txt"], "fallthrough": true }, { "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": true } ], // Live editing is OPT-IN: a deploy binds a LIVE_DO Durable Object // namespace whose class_name is the engine's LiveObject

  • free-tier-eligible ↗

    { "$schema": "./node_modules/wrangler/config-schema.json", "name": "open-artifacts", "main": "src/index.ts", "compatibility_date": "2026-07-03", "workers_dev": true, "assets": { "directory": "./public", "binding": "ASSETS", "run_worker_first": ["/", "/api/*", "/a/*", "/og/*", "/fonts/*", "/vendor/*"] }, // "1" opts this deploy into the web-font surface: the /fonts proxy, // allowlisted font CDNs, and an allow-sam

  • free-tier-eligible ↗

    the viewer stays unchanged. "vars": { "OPEN_ARTIFACTS_WEB_FONTS": "1", "OPEN_ARTIFACTS_HANDOFF": "1" }, "d1_databases": [ { "binding": "DB", "database_name": "open-artifacts", "database_id": "a01d21cd-86cb-4832-872d-d196e6918a3b" } ], "r2_buckets": [ { "binding": "CONTENT", "bucket_name": "open-artifacts-content" } ], "rules": [ { "type": "Text", "globs": ["**/*.txt"], "fallthrough": true }, { "type": "CompiledWasm", "globs": ["**/*

  • free-tier-eligible ↗

    "database_name": "open-artifacts", "database_id": "a01d21cd-86cb-4832-872d-d196e6918a3b" } ], "r2_buckets": [ { "binding": "CONTENT", "bucket_name": "open-artifacts-content" } ], "rules": [ { "type": "Text", "globs": ["**/*.txt"], "fallthrough": true }, { "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": true } ], // Live editing is OPT-IN: a deploy binds a LIVE_DO Durable Object // namespace whose class_name is the engine's LiveObject

  • free-tier-eligible ↗

    up>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 seconds)<br> Max of 15 minutes of CPU time per [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) or [Queue Consumer](https://developers.cloudflare.co

  • free-tier-eligible ↗

    oudflare.com/workers/platform/pricing/#workers) | | --- | --- | --- | | Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows | | Rows written | 100,000 / day | First 50 million / month included + $1.00 / million rows | | Storage (per GB stored) | 5 GB (total) | First 5 GB included + $0.75 / GB-mo | Track your D1 usage To accurately track your usage, use the [meta object](https://developers.cloudflare.com/d1/worker-api/return-object/), [GraphQL Analytics API](https://developers.cloudflare.com/d1/observability/metrics-analytics/#query-via-the-graphql-api), or the [Cloudflare dashboard ↗︎](https://dash.cloudflare.com/?to=/:account/workers/d1/). Select your D1 database, then vie

  • free-tier-eligible ↗

    infrequent access storage) for 1.1 GB, you will be billed for 2 GB. ### Free tier You can use the following amount of storage and operations each month for free. | | Free | | --- | --- | | Storage | 10 GB-month / month | | Class A Operations | 1 million requests / month | | Class B Operations | 10 million requests / month | | Egress (data transfer to Internet) | Free <sup>[1](#user-content-fn-1)</sup> | Caution The free tier only applies to Standard storage, and does not apply to Infrequent Access storage. ### Storage usage Storage is billed using gigabyte-month (GB-month) as the billing metric. A GB-month is calculated by averaging the *peak* storage per day over a billing period (30 days). For examp

  • MIT ↗

    MIT License Copyright (c) 2026 Frad Ser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGE

  • architecture ↗

    { "$schema": "./node_modules/wrangler/config-schema.json", "name": "open-artifacts", "main": "src/index.ts", "compatibility_date": "2026-07-03", "workers_dev": true, "assets": { "directory": "./public", "binding": "ASSETS", "run_worker_first": ["/", "/api/*", "/a/*", "/og/*", "/fonts/*", "/vendor/*"] }, // "1" opts this deploy into the web-font surface: the /fonts proxy, // allowlisted font CDNs, and an allow-sam

  • architecture ↗

    the viewer stays unchanged. "vars": { "OPEN_ARTIFACTS_WEB_FONTS": "1", "OPEN_ARTIFACTS_HANDOFF": "1" }, "d1_databases": [ { "binding": "DB", "database_name": "open-artifacts", "database_id": "a01d21cd-86cb-4832-872d-d196e6918a3b" } ], "r2_buckets": [ { "binding": "CONTENT", "bucket_name": "open-artifacts-content" } ], "rules": [ { "type": "Text", "globs": ["**/*.txt"], "fallthrough": true }, { "type": "CompiledWasm", "globs": ["**/*

  • architecture ↗

    "database_name": "open-artifacts", "database_id": "a01d21cd-86cb-4832-872d-d196e6918a3b" } ], "r2_buckets": [ { "binding": "CONTENT", "bucket_name": "open-artifacts-content" } ], "rules": [ { "type": "Text", "globs": ["**/*.txt"], "fallthrough": true }, { "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": true } ], // Live editing is OPT-IN: a deploy binds a LIVE_DO Durable Object // namespace whose class_name is the engine's LiveObject

What it can replace

Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.

external SaaS target
varies
→ D1 + R2 + Workers

How it works

The shape of Open Artifacts on Cloudflare, and how it stacks up against the rented tools it replaces.

Architecture

Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.

View upstream source ↗
Public interface
Configured entry points2
open-artifacts
wrangler.jsonc
open-artifacts
wrangler.dev.jsonc
↓
App
open-artifacts
entry
Cloudflare Workers
Entrypoint: src/index.ts
open-artifacts
entry
Cloudflare Workers
Entrypoint: src/index.ts
↓

Configuration and workflow sources

Reviewed commit 4f8b54bec3af. Files were read as data; upstream applications and CI jobs were not executed.

Deployment configuration · 2 files
wrangler.jsonc ↗

Cloudflare Workers · compatibility 2026-07-03

open-artifacts · default

Entrypoint: src/index.ts

Static assets: ./public · Worker first: ["/","/api/*","/a/*","/og/*","/fonts/*","/vendor/*"]

  • DB → D1
  • CONTENT → R2
  • ASSETS → Static assets
wrangler.dev.jsonc ↗

Cloudflare Workers · compatibility 2026-07-03

open-artifacts · default

Entrypoint: src/index.ts

Static assets: ./public · Worker first: ["/","/api/*","/a/*","/og/*","/fonts/*","/vendor/*"]

  • DB → D1
  • CONTENT → R2
  • LIVE_DO → Durable Objects · class LiveObject
  • ASSETS → Static assets

Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.

Runtime source · handlers, binding usage and workflow steps

Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.

src/app.ts ↗
  • L99 · app.use("*")
  • L106 · app.route("/api")
  • L109 · app.route("/api")
  • L110 · app.route("/api")
  • L112 · app.get("/")
  • L120 · app.get("/fonts/:slug{[a-z0-9-]+\\.(?:woff2|css)}")
  • L154 · app.get("/vendor/mermaid.runtime.js")
  • L176 · app.get("/vendor/mediapipe/*")
  • L205 · app.get("/a/:id")
  • L316 · app.get("/a/:id/frame")
  • L356 · app.get("/og/:id")
  • L393 · app.get("/health")
  • L54 · resolveRecord calls (conditional paths may differ): store.get, parseVersionParam, store.listVersions, versions.find, store.getContentMeta
  • L94 · createApp calls (conditional paths may differ): app.use, c.set, next, app.route, app.get, c.env.ASSETS.fetch, hasBrandConfig, includes, asset.headers.get, brandHomepage, c.req.param, raw.endsWith, raw.slice, fontFaceCss, parseSlug, materializeFont, path.endsWith, storeFrom, brandFor, statusThemeFor

Environment references: c.env.ASSETS · c.env.OPEN_ARTIFACTS_WEB_FONTS · c.env.BRAND_URL · c.env.LIVE_DO

src/api.ts ↗
  • L227 · api.post("/artifacts")
  • L311 · api.put("/artifacts/:id")
  • L370 · api.delete("/artifacts/:id")
  • L378 · api.get("/artifacts/:id")
  • L407 · api.get("/artifacts/:id/raw")
  • L462 · api.get("/artifacts/:id/comments")
  • L473 · api.post("/artifacts/:id/comments")
  • L587 · api.patch("/artifacts/:id/comments/:commentId")
  • L620 · api.delete("/artifacts/:id/comments/:commentId")
  • L642 · api.patch("/artifacts/:id")
  • L68 · resolveMaxContentBytes calls (conditional paths may differ): test, Number.parseInt
  • L113 · bearerToken calls (conditional paths may differ): c.req.header
  • L125 · authorizeWrite calls (conditional paths may differ): store.get, c.json, bearerToken, sha256Hex, looksLikeChannelToken, timingSafeEqual, authorizeWrite, c.get
  • L166 · parseVersionParam calls (conditional paths may differ): test, Number
  • L186 · publishToChannel calls (conditional paths may differ): store.update, broadcastVersionIfLive, c.json, artifactUrl, store.get
  • L566 · authorizeCommentMutation calls (conditional paths may differ): bearerToken, sha256Hex, timingSafeEqual, authorizeWrite

Environment references: env.MAX_CONTENT_MIB · c.env.DB · c.env.CONTENT · c.env.PUBLIC_URL · c.env.OPEN_ARTIFACTS_HANDOFF

src/authorizer.ts ↗
  • L29 · bearerToken calls (conditional paths may differ): c.req.header
src/domain.ts ↗
  • L154 · isEmojiFavicon calls (conditional paths may differ): segment, segments.every, EMOJI_GRAPHEME.test
  • L163 · contentByteLength calls (conditional paths may differ): encode
  • L167 · extractTitle calls (conditional paths may differ): content.match
  • L184 · validateEncryption calls (conditional paths may differ): invalid, BASE64.test, Number.isInteger
  • L217 · validateCommon calls (conditional paths may differ): invalid, contentByteLength, validateEncryption, BASE64.test, isEmojiFavicon
  • L299 · validateCreate calls (conditional paths may differ): validateCommon, invalid, extractTitle
  • L334 · validateUpdate calls (conditional paths may differ): validateCommon, Number.isInteger, invalid
  • L377 · validateAnchorVersion calls (conditional paths may differ): Number.isInteger
  • L383 · validateAnchor calls (conditional paths may differ): invalid, validateAnchorVersion, Number.isFinite, ctx, Number.isInteger, contentByteLength, JSON.stringify
  • L445 · validateComment calls (conditional paths may differ): invalid, contentByteLength, validateAnchor
src/fonts.ts ↗
  • L23 · parseSlug calls (conditional paths may differ): slug.split, Number.parseInt, Number.isInteger
  • L62 · resolveFontshareFile calls (conditional paths may differ): fetch, searchRes.json, find, toLowerCase, slug.family.toLowerCase, fontRes.json, filter, styles.filter, chosen.file.startsWith
  • L111 · materializeFont calls (conditional paths may differ): env.CONTENT.get, fontR2Key, cached.arrayBuffer, parseSlug, resolveFontshareFile, fetch, fontRes.arrayBuffer, env.CONTENT.put, String
  • L139 · fontFaceCss calls (conditional paths may differ): parseSlug, join, map, parsed.family.split, toUpperCase, w.charAt, w.slice, JSON.stringify

Environment references: env.CONTENT

src/handoff-api.ts ↗
  • L83 · handoffApi.get("/artifacts/:id/handoffs")
  • L98 · handoffApi.post("/artifacts/:id/handoffs")
  • L183 · handoffApi.get("/artifacts/:id/handoffs/:hid/media")
  • L206 · handoffApi.get("/artifacts/:id/handoffs/:hid/events")
  • L233 · handoffApi.delete("/artifacts/:id/handoffs/:hid")
  • L36 · tryParseJson calls (conditional paths may differ): JSON.parse
  • L44 · stableMediaType calls (conditional paths may differ): type.split, test
  • L54 · viewAuthRecord calls (conditional paths may differ): storeFrom, store.get, authorizeView, c.get

Environment references: c.env.OPEN_ARTIFACTS_HANDOFF

src/home.ts ↗
  • L35 · hasBrandConfig calls (conditional paths may differ): Boolean
  • L46 · brandFor calls (conditional paths may differ): hasBrandConfig, trim, toUpperCase
  • L66 · brandHomepage calls (conditional paths may differ): hasBrandConfig, brandFor, transform, on, setText, el.setAttribute, el.setInnerContent

Environment references: env.BRAND_NAME · env.BRAND_STATUS_THEME · env.BRAND_WORDMARK · env.BRAND_TAGLINE · env.BRAND_DESCRIPTION · env.BRAND_LEAD · env.BRAND_CHIP

src/live-api.ts ↗
  • L125 · liveApi.get("/artifacts/:id/live")
  • L135 · liveApi.get("/artifacts/:id/live/poll")
  • L156 · liveApi.get("/artifacts/:id/live/status")
  • L164 · liveApi.post("/artifacts/:id/live/heartbeat")
  • L172 · liveApi.post("/artifacts/:id/live/consume-exit")
  • L180 · liveApi.put("/artifacts/:id/live")
  • L254 · liveApi.post("/artifacts/:id/live/edit-stash")
  • L296 · liveApi.get("/artifacts/:id/live/edit-stash")
  • L307 · liveApi.delete("/artifacts/:id/live/edit-stash")
  • L317 · liveApi.post("/artifacts/:id/live/edit-commit")
  • L337 · liveApi.delete("/artifacts/:id/live/events/:eid")
  • L355 · liveApi.post("/artifacts/:id/live/reply")
  • L43 · clampLivePollTimeout calls (conditional paths may differ): Number, Number.isFinite, Math.min, Math.max
  • L52 · liveEnabled calls (conditional paths may differ): Boolean
  • L60 · authorizeLive calls (conditional paths may differ): storeFrom, store.get, c.req.query, parseVersionParam, c.get, authorizer.authorizeView
  • L88 · stubFor calls (conditional paths may differ): liveNs.getByName, liveNs.get, liveNs.idFromName
  • L112 · broadcastVersionIfLive calls (conditional paths may differ): liveEnabled, rpcBroadcastVersion, stubFor

Environment references: c.env.LIVE_DO

src/og.ts ↗
  • L14 · decodeBase64 calls (conditional paths may differ): atob, binary.charCodeAt
  • L34 · ensureWasm calls (conditional paths may differ): catch, initWasm
  • L47 · renderOgCardPng calls (conditional paths may differ): ensureWasm, ogCardSvg, resvg.render, rendered.asPng, rendered.free, resvg.free
src/store.ts ↗
  • L283 · ensureSchema calls (conditional paths may differ): schemaReady.get, db.batch, SCHEMA.map, db.prepare, run, isExpectedMigrationError, console.warn, catch, schemaReady.delete, schemaReady.set
  • L325 · ensureSchemaForTests calls (conditional paths may differ): ensureSchema
  • L333 · resetSchemaMemoForTests calls (conditional paths may differ): schemaReady.delete
  • L401 · contentObjectBody calls (conditional paths may differ): JSON.stringify
  • L1074 · toComment calls (conditional paths may differ): JSON.parse
src/tokens.ts ↗
  • L5 · generateId calls (conditional paths may differ): crypto.getRandomValues, chars.push, chars.join
  • L19 · base64Url calls (conditional paths may differ): replace, replaceAll, btoa, String.fromCharCode
  • L26 · generateWriteToken calls (conditional paths may differ): base64Url, crypto.getRandomValues
  • L30 · generateChannelToken calls (conditional paths may differ): base64Url, crypto.getRandomValues
  • L34 · looksLikeChannelToken calls (conditional paths may differ): value.startsWith
  • L38 · sha256Hex calls (conditional paths may differ): crypto.subtle.digest, encode, join, map, padStart, b.toString
  • L48 · timingSafeEqual calls (conditional paths may differ): encoder.encode, crypto.subtle.timingSafeEqual
Build and deployment pipeline · 0 GitHub Actions workflows

Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.

No GitHub Actions workflow was found in the collected tree. Deployment may be manual or configured elsewhere.

package.json ↗
  • deploy: wrangler deploy

Full upstream document by @coda0HQ · README.md · snapshot 4f8b54b

Open Artifacts

License: MIT Node

English | 简体中文

Open-source, self-hosted Claude Code Artifacts: let any coding agent publish self-contained HTML/Markdown pages to shareable URLs, protect them with passwords (zero-knowledge, client-side encryption), and keep them updated as the project they describe evolves. Runs entirely on Cloudflare (Workers + D1 + R2), fits in the free tier, no accounts anywhere.

Hosted or self-hosted. coda0.com is the official managed instance, run by the project — point your agent at it for zero-setup publishing. Or self-host the engine on your own Cloudflare account (see below); it's the same MIT-licensed code either way.

flowchart LR
  you["you"] -- "share the app's interaction flows as a page" --> agent["your agent"]
  agent -- "POST /api/artifacts" --> worker["your Worker"]
  worker -- "https://<instance>/a/3fKx9mQp2Wvb" --> url["shareable URL"]
  later["later: the flows change"] --> status["agent runs<br/>artifact.mjs status"]
  status -- "stale" --> regen["regenerates page"]
  regen -- "PUT (same id)" --> worker2["your Worker"]
  worker2 -- "same URL, v2" --> url

Give your agent the skill

npx skills add coda0HQ/open-artifacts -s using-open-artifacts   # project scope (.claude/skills/)
npx skills add coda0HQ/open-artifacts -s using-open-artifacts -g  # or user scope

Works with Claude Code and any agent supporting the Agent Skills standard. Then point it at an instance — the hosted one, or your own:

export OPEN_ARTIFACTS_URL=https://coda0.com   # hosted; or your self-hosted URL

No instance yet? references/deployment.md (bundled with the skill) lists three ways to get one: use the public shared instance with zero setup, self-host on your own Cloudflare account, or share a team instance, with a trust-model table for picking based on content sensitivity.

The bundled SKILL.md and references/design.md teach the agent the design philosophy: an expert-designer workflow (understand, explore, plan, build, verify), an explicit anti-AI-slop list, modern CSS power moves, and a 5-direction library (Editorial / Modern minimal / Human / Tech utility / Brutalist) with ready-to-paste OKLch palettes and font stacks for when no brand is specified. references/tokens.css is the shared token contract the Recipe builder injects into every HTML artifact before its theme fragment. The skill also provides a quality profile for responsive HTML builds and an optional Reference DNA workflow that records approved design facts as a static Recipe input without bringing source assets or network requests into the Artifact. The quality profile rejects recurring HTML defects such as fake browser chrome, unsafe sticky stacks, overflowing display text, and unconstrained media grids. Adapted from open-design, Claude's artifact-design skill, impeccable by Paul Bakaus (Apache-2.0, interaction-state and anti-pattern rules), Emil Kowalski (easing, frequency, and duration rules), and Apple WWDC 2018 Designing Fluid Interfaces (canvas gesture physics). Retargeted to this project's strict no-external-requests CSP.

Ask your agent to "publish this as an artifact" — it runs the bundled CLI:

node skills/using-open-artifacts/scripts/artifact.mjs validate \
  .artifacts/recipes/app-interactions.recipe.json
node skills/using-open-artifacts/scripts/artifact.mjs smoke \
  .artifacts/recipes/app-interactions.recipe.json
node skills/using-open-artifacts/scripts/artifact.mjs create \
  .artifacts/recipes/app-interactions.recipe.json

Every artifact is generated from a versioned JSON Recipe plus ordered fragments. The Recipe owns title, favicon, format, scope, watch globs, channel, level, Canvas mode, locality, and encryption policy. create and update compose and validate in memory, then send exactly one final publish request. For a responsive scrolling HTML artifact, smoke renders the composed output at 320, 375, 414, and 768px with agent-browser before publishing; it detects horizontal scrolling and heading overflow without writing project state.

Deploy your own instance

git clone https://github.com/coda0HQ/open-artifacts && cd open-artifacts
pnpm install
npx wrangler d1 create open-artifacts        # put database_id into wrangler.jsonc
npx wrangler r2 bucket create open-artifacts-content
pnpm run deploy

The schema applies itself on first request — no migration step. To restrict who can create artifacts on your instance (updates are always restricted by per-artifact write tokens):

npx wrangler secret put CREATE_TOKEN         # then set OPEN_ARTIFACTS_TOKEN client-side

Local development: pnpm dev (state persists in .wrangler/state).

How it works

Concern Design
Identity No accounts. Artifact ids are 12-char crypto-random (unguessable, unlisted). Creation returns a one-time writeToken; only its SHA-256 is stored.
Deterministic sources A strict Recipe plus ordered fragments generates every Artifact. The builder injects tokens and, for Canvas, the vendored runtime and controls. Manifest v2 records Recipe/input/output hashes; direct HTML/Markdown CLI publishing is rejected.
Quality checks HTML builds reject selected structural defects: non-token visual values, fake device/browser chrome, unsafe sticky stacks, display-text overflow risks, wrapping primary actions, and unconstrained media grids. smoke renders scrolling HTML at 320, 375, 414, and 768px to catch horizontal scroll and heading overflow. Canvas, Markdown, and React keep their dedicated rendering contracts.
Reference DNA An approved document.referenceDna sidecar stores inert design facts and provenance after explicit user attestation. It contributes to Recipe input hashes and stale detection but is never injected into the published page. Shared sidecars live in .artifacts/reference-dna/; local or encrypted sidecars live in .artifacts/reference-dna.local/.
Channels artifact.channel binds an artifact to a stable URL. The CLI keeps a per-channel token (ch_) in .artifacts/credentials.json; presenting it on a later create updates the bound artifact (new version, same link) instead of minting a new one. Only the channel hash is stored server-side.
Local mode artifact.local: true places private sources under gitignored .artifacts/recipes.local/ and .artifacts/fragments.local/, with state in manifest.local.json. Shared Recipes/fragments live under .artifacts/recipes/ and .artifacts/fragments/ and may be committed. Encrypted Recipes are always private.
Storage D1 for metadata/tokens/version index, R2 for content bodies (content/<id>/<version>). Both strongly consistent — updates are visible immediately.
Versions Every publish is an immutable version with an optional label and its own title, description, favicon, format, and encryption state, so history reflects what each version actually looked like. ?v=N views history; PUT accepts baseVersion and returns 409 on conflicts (override with force).
Serving The Worker wraps stored content in a skeleton (CSS reset, emoji favicon, viewport, light/dark theme with a data-theme toggle) and serves it with Content-Security-Policy: sandbox allow-scripts ...; default-src 'none' — artifact scripts run in an opaque origin and cannot make any external request.
Link previews Every page emits OpenGraph + Twitter tags (title, description, image). GET /og/:id returns a 1200x630 PNG card rasterized on the edge with @resvg/resvg-wasm from an embedded Inter subset — a real raster crawlers render (they ignore SVG), self-contained with no external requests.
Passwords The CLI encrypts locally: PBKDF2-HMAC-SHA256 (600k iterations) + AES-256-GCM. The server stores only {salt, iv, ciphertext}. The viewer serves an unlock shell that decrypts in the browser and renders the result inside a sandboxed iframe. The password never leaves the client.
Auto-update The Recipe records scope, watch, and autoUpdate; Manifest v2 keeps the publication snapshot. artifact.mjs status reports stale watched artifacts, while the optional Stop-hook path only surfaces opted-in entries. Agents update Recipe fragments or ack reviewed drift.
Markdown Rendered client-side (vendored marked, inlined — no CDN), so encrypted Markdown works without the server ever seeing plaintext.

API

POST   /api/artifacts           { content, favicon, title?, description?, format?, label?, encrypted?, channel? }
                                → 201 { id, url, writeToken, version, channel? }
PUT    /api/artifacts/:id       same fields + baseVersion?/force?   (Bearer writeToken or channel token)
GET    /api/artifacts/:id       metadata + version history
GET    /api/artifacts/:id/raw   stored content (?v=N)
DELETE /api/artifacts/:id       (Bearer writeToken)
GET    /a/:id                   rendered page (?v=N)

encrypted is { salt, iv, iterations } (all base64/int) with base64 ciphertext as content. channel is a channel token (ch_...) that targets the artifact already bound to that channel, or binds a new one on first use. Max content size 4 MiB.

Security model

  • Serving untrusted HTML on your own origin is the classic stored-XSS trap; every user-content response here carries the CSP sandbox directive (opaque origin — no cookies, no storage, no same-origin API calls) plus default-src 'none', connect-src 'none', X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer.
  • *.workers.dev is on the Public Suffix List, isolating your instance from other sites.
  • Anyone with the URL of an unprotected artifact can read it (like an unlisted gist). Use --password for anything sensitive; title/favicon metadata stays plaintext.
  • An open instance (no CREATE_TOKEN) lets anyone with the URL create pages. Set the secret for anything public-facing.

Development

pnpm test          # Worker integration tests (vitest + workerd)
pnpm test:cli      # skill CLI tests
pnpm typecheck
pnpm check         # biome lint + format

Install agent-browser to run artifact.mjs smoke; unit tests use a stub and do not require its browser binary.

BDD scenarios live in tests/features/; the architecture decision record in docs/architecture.md.

MIT licensed.

Frequently asked about Open Artifacts

What is Open Artifacts?+

Open Artifacts is a self-hosted Claude Artifacts alternative built on the Cloudflare developer platform. Publish versioned HTML and Markdown artifacts from a Cloudflare Worker.

What does Open Artifacts replace?+

Open Artifacts is listed as an alternative to Claude Artifacts. Compare the features and tradeoffs before migrating.

What Cloudflare primitives does Open Artifacts use?+

Open Artifacts is built on D1, R2, Workers.

How much does Open Artifacts cost to run?+

The documented Open Artifacts deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs. Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits. Keep aggregate D1 use below 5 million rows read/day, 100,000 rows written/day and 5 GB total storage; a request can touch many rows. Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account. Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations. Check current Cloudflare pricing before deploying.

Is Open Artifacts open source?+

The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/coda0HQ/open-artifacts/4f8b54bec3af00ebb7e0750aa052ca7147c0f039/LICENSE. Source code and contributor credit are available at https://github.com/coda0HQ/open-artifacts.

Discussion · 0

sign in to comment →
No comments yet — be the first.