cairn artifact server
GitHub

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.

install
$ curl -fsSL https://aloisdeniel.github.io/cairn/install.sh | sh
one Go binary zero external deps pure-Go SQLite data = a directory

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.

terminal
$ 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.

register.db — mode: loading

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.

    register — source
    <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

    terminal
    # 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
    docker — instead of the installer
    # 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
    your artifact — index.html
    <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.

    cairnclaude artifacts
    Self-hosted on your domain and diskHosted on claude.ai
    Full multi-file SPAs, no CSP wallSingle-page sandbox
    First-class SQLite (raw SQL, transactions, migrations) + per-version file storageCapability-gated runtime
    Explicit versions, changelogs, stable URLsProduct history
    Any agent via CLI / API keys — model-agnosticClaude only
    Trust-based, for invited usersSandboxed, for untrusted viewers