cairn / self-hosted artifact server / open source
Push a web app. Get a versioned URL. One database, shared by everyone.
Cairn serves artifacts — single-page apps whose visitors all read and write the same SQLite database — from one Go binary: server, admin UI, and an agent-oriented CLI. The perfect home for the small internal tools and personal apps you build with AI — an open, self-hosted alternative to Claude Artifacts for small trusted teams. And because any agent can drive the CLI, Claude, Codex and the rest all contribute to the same collection.
Not a public web host. Cairn is an internal hosting platform: tools your team signs into, backed by shared data — or simple demos you make public to show off. It is not built to host dynamic websites for the open internet.
$ curl -fsSL https://aloisdeniel.github.io/cairn/install.sh | sh
01 / how it works
Push, serve, share
A directory with an index.html becomes a versioned artifact.
Nothing else to configure.
01push
The CLI zips your build directory and uploads it as a new version — or overwrites one in place while you iterate.
02serve
Every version gets a stable URL — /artifacts/{id}/{versionId}/ — with a name and changelog. Re-uploads never touch the data.
03share
Each version owns a shared SQLite database and a file storage, reached from the page via cairn.js. All visitors see the same data.
$ cairn push ./dist --artifact my-app --create --public $ cairn push ./dist --artifact my-app --overwrite latest $ cairn open my-app
02 / features
What you get
The terse version. The README has the rest.
| one binary | Server, admin UI, and CLI in a single Go executable. Pure-Go SQLite, zero external dependencies. All state lives in one directory on disk (--data-dir). |
|---|---|
| versions | Explicit versions with names and changelogs. Stable URLs per version; re-uploadable in place while iterating. The shared database survives re-uploads. |
| shared sqlite | Every version owns a lazily-created SQLite database shared by all visitors — raw SQL over HTTP via cairn.db.query / batch / migrate. Cross-version read-only queries let you migrate data forward, and the .db file is downloadable. |
| file storage | Each version also has a file storage for the binary data that doesn't belong in SQLite — images, exports, attachments. Upload, list, download and delete via cairn.files or the CLI; like the database, it survives re-uploads and is readable anonymously on public artifacts. |
| agent cli | Every AI agent contributes to the same collection. Claude Code, Codex, Gemini CLI — anything that can run a command. Set CAIRN_HOST + CAIRN_API_KEY and every command works headlessly, all with --json; each agent gets its own revocable API key. Attach a session id as a resource and it works anywhere an artifact id does. A Claude Code skill ships in the repo. |
| cairn.js | One client API, two backends. On a Cairn server it proxies to the HTTP APIs; on any static host it runs a local in-browser SQLite (debug mode, user {id: 0, name: "Debug"}) — you develop against the exact code you deploy. |
| authentication | No email verification, no invite tokens. An admin creates the account; the user picks their password at first sign-in. Sessions use signed tokens, agents use revocable API keys, and a password reset or disable invalidates every token instantly. |
| trust model | Admin-invited users, no self-signup. Every authenticated user can run raw SQL — built for a small circle of trusted people, not the open internet. Anonymous visitors get read-only access to public artifacts. |
03 / live demo
The register
This panel is cairn.js running on this static page in
debug mode: a real SQLite database in your browser, persisted to
IndexedDB — your entries stay on your machine. Publish this same file to
a Cairn server and the identical code becomes a shared, multi-user
register.
starting local backend (loads sql.js on first visit)…
Under the hood: cairn.ready() → cairn.db.migrate() once
for the schema → cairn.db.query() with ? placeholders
for insert and select. No SDK, no ORM — SQL.
<script src="./cairn.js"></script> <script> await cairn.ready(); // sql.js in the browser here; server APIs on Cairn // run-once schema — exactly what this page migrates await cairn.db.migrate('001-register', [{ sql: 'CREATE TABLE register (id INTEGER PRIMARY KEY AUTOINCREMENT, ' + 'name TEXT NOT NULL, note TEXT NOT NULL, created_at TEXT NOT NULL)' }]); // sign the register await cairn.db.query( 'INSERT INTO register (name, note, created_at) VALUES (?, ?, ?)', [name, note, new Date().toISOString()]); // latest entries + the live count in the nav const latest = await cairn.db.query( 'SELECT id, name, note, created_at FROM register ORDER BY id DESC LIMIT 6'); const count = await cairn.db.query('SELECT COUNT(*) FROM register'); // latest.rows → [[id, name, note, created_at], …] </script>
The rest of the panel is DOM plumbing. This exact code becomes a shared, multi-user register the moment the directory is pushed to a Cairn server.
04 / quick start
Server up in one minute
# install (or: docker, or: go install github.com/aloisdeniel/cairn/cmd/cairn@latest) $ curl -fsSL https://aloisdeniel.github.io/cairn/install.sh | sh # run the server — data is just this directory; no password on the CLI: # you choose the admin password in the browser at first sign-in $ cairn serve --data-dir data --admin-email you@example.com # log in, push, query — every command takes --json for agents $ cairn login --host http://localhost:8787 --email you@example.com $ cairn push ./dist --artifact my-app --create --public $ cairn db query --artifact my-app --json "SELECT COUNT(*) FROM notes" $ cairn files put ./report.pdf --artifact my-app --path exports/report.pdf
# prebuilt multi-arch image on GHCR — all state lives in the named volume $ docker run -p 8787:8787 -v cairn-data:/data \ -e CAIRN_ADMIN_EMAIL=you@example.com ghcr.io/aloisdeniel/cairn:latest # or: docker compose up -d — a docker-compose.yml ships in the repo
<script src="./cairn.js"></script> <script> await cairn.ready(); const me = await cairn.me(); // null when anonymous await cairn.db.migrate('001-schema', [ {sql: 'CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)'} ]); await cairn.db.query('INSERT INTO notes (body) VALUES (?)', ['hi']); const res = await cairn.db.query('SELECT * FROM notes'); // {columns, rows} await cairn.files.upload('photos/cat.png', blob); // per-version files img.src = cairn.files.url('photos/cat.png'); </script>
05 / compare
vs. Claude Artifacts
Different trade-offs, honestly stated.
| cairn | claude artifacts |
|---|---|
| Self-hosted on your domain and disk | Hosted on claude.ai |
| Full multi-file SPAs, no CSP wall | Single-page sandbox |
| First-class SQLite (raw SQL, transactions, migrations) + per-version file storage | Capability-gated runtime |
| Explicit versions, changelogs, stable URLs | Product history |
| Any agent via CLI / API keys — model-agnostic | Claude only |
| Trust-based, for invited users | Sandboxed, for untrusted viewers |