Self-hosted Claude artifacts on Cloudflare
Goal: every Claude surface you use (Desktop, Cowork, Code, CLI) can publish HTML and Markdown artifacts to your own Cloudflare account, and humans can open them at a URL on your domain with per-artifact visibility.
Decisions already made
| Question | Decision |
|---|---|
| Viewer access | Per artifact: private by default, optionally public or share-link |
| Domain | A subdomain on your existing Cloudflare zone, e.g. artifacts.<domain> |
| Plan | Workers Paid, Zero Trust free tier |
| Publish path | Remote MCP server for all surfaces, plus a wrangler-based skill for large assets |
Architecture
One Worker, one hostname, four responsibilities:
/mcp— remote MCP server (McpAgent, Streamable HTTP) behind OAuth 2.1. Claude connects here./a/<slug>— viewer. Renders Markdown to HTML on the fly, serves raw HTML under a strict CSP./assets/<slug>/<file>— R2 passthrough for images and data files./and/login— private index of your artifacts;/loginis the only path Cloudflare Access sits on.
Storage:
- R2 bucket
artifacts. Keys:<id>/v<n>/content.md|html,<id>/assets/<name>. Every publish writes a new version; nothing is overwritten. - D1 database
artifacts. Tables:artifacts(id, slug, title, description, type, visibility, current_version, timestamps),versions(artifact_id, n, r2_key, size, label, created_at),share_tokens(token, artifact_id, expires_at). - KV namespace
OAUTH_KVfor the OAuth provider’s grants and tokens. - Durable Object for McpAgent sessions (required by the Agents SDK).
Rendering:
- Markdown → HTML in the Worker with markdown-it, wrapped in a layout that ships light/dark tokens, a table of contents, and tabular numerics.
- Fenced blocks get upgraded client-side:
mermaid→ diagrams,chart(JSON spec) → Chart.js,vega-lite→ Vega, code → highlight.js,$$→ KaTeX. Libraries load from cdnjs, pinned. - Raw HTML artifacts are served as-is with a CSP that allows scripts from cdnjs/jsdelivr only and blocks
connect-src, mirroring Claude’s own artifact sandbox. - Responses are cached with the Cache API keyed by
<id>/v<n>, so a new version busts the cache naturally.
Auth, two layers
Claude → MCP server. @cloudflare/workers-oauth-provider in front of the MCP route. Its upstream identity provider is a Cloudflare Access for SaaS application (OIDC). Flow: Claude opens the authorize URL, you log in through Access (one-time PIN or your IdP), the Worker mints an MCP token. Same flow for every surface:
- Claude Code / CLI:
claude mcp add --transport http artifacts https://artifacts.<domain>/mcp - Claude Desktop / Cowork: Settings → Connectors → Add custom connector with the same URL.
Viewer → page. No hostname-level Access, because public artifacts must stay public. Instead:
- Access self-hosted app protects only
artifacts.<domain>/login. Visiting it sets theCF_Authorizationcookie for the whole hostname and redirects back. - For private artifacts the Worker verifies that cookie’s JWT against your team domain’s JWKS (jose, or the newer
ctx.accesshelper if available). Missing or invalid → redirect to/login?next=. - Share links:
/a/<slug>?k=<token>where the token is a random 128-bit value stored in D1 with an optional expiry. - Public artifacts skip all checks.
MCP tools
| Tool | Purpose |
|---|---|
publish |
slug (optional), title, description, type (md/html), content, visibility. Creates or adds a version. Returns URL. |
update |
Same as publish against an existing slug. |
get |
Returns current content and metadata, so Claude can build on the live version. |
list |
Recent artifacts with URLs. |
set_visibility |
private / public / share; returns share link when relevant. |
delete |
Soft delete (keeps R2 objects for a grace period). |
attach_asset |
Small assets inline (base64, up to ~2 MB). Larger files go through wrangler and are registered with this tool by key. |
Claude-side skill
A small skill (installed in ~/.claude/skills, and packaged as a plugin for Desktop/Cowork) that mirrors the built-in Artifact tool’s rules: when a deliverable deserves a page, how to title it, always redeploy to the same slug, prefer Markdown for documents and HTML for apps, and how to push big assets with wrangler r2 object put then attach_asset.
Phases
- Prerequisites. Authorize the Cloudflare API MCP servers in this session or confirm details by hand: zone name, Zero Trust org and team domain, an IdP or one-time PIN enabled. Pick the subdomain and a project directory.
- Skeleton and viewer. Worker with R2 + D1 bindings, custom domain route, Markdown rendering, public artifacts only. Upload a test file with wrangler and open it. This is the first visible win.
- MCP server with OAuth. McpAgent + OAuth provider + Access for SaaS app. Connect from Claude Code first, then Desktop. Publish a page from a chat.
- Skill, first cut. Write the skill against the tools that exist so far and use it daily; real use shapes the later phases. Tool descriptions and skill text live in the repo and are versioned together.
- Visibility. Access app on
/login, JWT verification, share tokens, the private index page. - Rich rendering. Mermaid, chart and vega blocks, highlighting, KaTeX, TOC, dark mode, CSP for raw HTML, asset serving, wrangler asset flow.
- Hardening. Size and rate limits, structured logs with observability enabled, R2 lifecycle rule for deleted artifacts, Vitest with the Workers pool, a deploy from CI.
Phases 2 and 3 are the ones worth doing first; everything after is incremental.
Open questions and risks
- Custom connectors in Claude Desktop/Cowork require a Pro, Max, or Team plan. Confirm yours allows them.
- Content travels inline in MCP tool calls. A few MB is fine; anything larger uses the wrangler path.
- The Agents SDK and OAuth provider libraries move fast. Versions and exact APIs get verified against the docs MCP at build time, not from memory.
- Where should the repo live? Suggest
~/projects/personal/cf-artifactsor similar. - Do you want Cowork and Desktop to share one connector, or separate ones with separate tokens? One is simpler; separate is easier to revoke.