Self-hosted artifact publishing: POST HTML/JSX/Markdown/zip, get an unguessable URL on your domain
AI assistants produce a lot of shareable output: dashboards, prototypes, reports, small apps. Claude's hosted artifacts work well, but the URLs live on someone else's infrastructure. This is the self-hosted version, about 1,100 lines. You POST content, it serves the rendered result at an unguessable URL on a domain you control.
It runs as one container with a single admin account and, by default, no database. Each artifact is a directory of plain files under /data, so backing up that directory backs up everything. On hosts that wipe local disk on restart, point it at durable external storage instead (an S3-compatible bucket, a git remote, or Postgres) by setting STORAGE_BACKEND. See deploying.
/ (behind an admin login) and a CLI.read / publish / full) with optional expiry, so you never share one master secret.private, shared through a signed capability link (?k=…) you can rotate to revoke. Switch any artifact to public (anyone with the bare link) or password-protected. Nothing is discoverable by default: unguessable slugs, noindex everywhere. See visibility.?raw=1 serves the bare content. Redirects are never framed.type: "redirect" turns a slug into a short link that answers an HTTP 301 at the server, where other hosts fall back to a JavaScript bounce. The target must be an absolute http(s) URL and cannot carry credentials, and the response is uncacheable, so repointing the slug takes effect on the next visit. Repoint one from the row menu in the dashboard, or with a PUT. Read the cost of running an open redirector on your domain first: docs/formats.md.<iframe>. The row menu writes the tag for you, pointing at ?raw=1 so the frame holds the artifact and not a second toolbar. Private and password-protected artifacts stay unembeddable on purpose, and the unlock prompt refuses to be framed at all. See docs/embedding.md.GET /api/artifacts/:slug/qr, or artifacts qr <slug>. SVG or PNG, generated in-process: no QR library, no third-party image service, nothing about your artifacts leaves the box. See the API reference.TRUST_PROXY for the real client IP behind Cloudflare or a reverse proxy. See deploying.Clone, configure, start:
git clone https://github.com/anvilnine/artifacts && cd artifacts
cp .env.example .env # set ARTIFACTS_API_KEY (openssl rand -hex 32) and BASE_URL
docker compose up -d
Or skip the clone and run the image directly:
docker run -d -p 3000:3000 -v artifacts-data:/data \
-e ARTIFACTS_API_KEY=$(openssl rand -hex 32) \
-e BASE_URL=https://artifacts.example.com \
ghcr.io/anvilnine/artifacts:latest
Publish something:
curl -s -X POST https://artifacts.example.com/api/artifacts \
-H "Authorization: Bearer $ARTIFACTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "<h1>hello</h1>", "type": "html", "slug": "hello", "tags": ["demo"]}'
# {"slug":"hello","url":"https://artifacts.example.com/a/hello"}
Let Claude Code publish for you:
claude mcp add --transport http artifacts https://artifacts.example.com/mcp \
--header "Authorization: Bearer ${ARTIFACTS_API_KEY}" --scope user
| I want to… | Read |
|---|---|
| Deploy it (Docker, compose, Coolify, bare node, env vars) | docs/deploy.md |
| Use the REST API (incl. zip sites and tags) | docs/api.md |
| Publish from the terminal | docs/cli.md |
| Hook up Claude Code / Codex / any agent | docs/mcp.md |
| Set up login + scoped API keys | docs/auth.md |
| Understand JSX/TSX rendering + zip validation | docs/formats.md |
| Put an artifact inside another page | docs/embedding.md |
No build step. Node ≥ 22.
npm install
cp .env.example .env # any ARTIFACTS_API_KEY works locally, e.g. "test"
npm run dev
# UI at http://localhost:3000
The end-to-end suite is one shell script:
bash .github/workflows/smoke.sh http://localhost:3000 <your-key>
A few cases cannot be reached over HTTP, such as a hand-edited auth.json. Those run as unit
tests on node's built-in runner, no server needed:
npm test
That covers the dashboard too. dashboard-check.mjs fetches /, parses the inline script in
public/index.html, and checks that every element the script grabs by id is still in the markup.
It runs no browser, so it catches a syntax error or a deleted element but not a runtime error.
PRs welcome. See CONTRIBUTING.md.
Self-hosting is the product, and the open source version stays complete. If you would rather not run a server, Anvil Nine is building a managed version: same publishing flow, custom domains, backups handled for you. A waitlist page opens with the first cloud announcement; watch this repo's releases until then.
Uploaded HTML runs in the browser. That is the whole point, so the model is built to contain it:
noindex, but there is no listing to browse. Don't publish secrets.publish scope gets a 256 kB body parser instead of the 10 MB one, and a cap of 20 large bodies a minute per client IP, both checked before the body is parsed. Put a CDN or edge limiter in front for volumetric attacks.Full threat model in SECURITY.md.
This project stands on other people's open source. All of the following are MIT licensed.
Runtime
Optional storage backends (loaded only when selected)
The SQLite backend uses Node's built-in node:sqlite (no dependency).
Web UI
MIT © 2026 Zonily Jame
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
docker run -i --rm ghcr.io/kuyazee/artifacts:latestMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-kuyazee-artifacts": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/kuyazee/artifacts:latest"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceghcr.io/kuyazee/artifacts:latestdockerArtifacts works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.