← All projects

Case study

Cairn

AI can draft, but only a human can commit - and every change is provable after the fact.

Based on a production appSanitized reference implementationMIT
  • TypeScript (strict)
  • Next.js 16 (App Router)
  • React 19
  • PostgreSQL + Row-Level Security
  • Zod
  • Vitest
  • GitHub Actions + CodeQL
This repository is a sanitized reference implementation inspired by production experience building a privacy-first personal record-keeping app. It ships none of that app's prompts, schema, data, or business logic - everything here is synthetic, and the domain is deliberately neutral so the engineering patterns are the point.

What it is

A working, tested system for personal records where an assistant helps but is never in charge. You write a rough note; the model proposes a clean entry; nothing becomes a record until you review and approve it. Every step - the proposal, your edits, your decision, the commit - is appended to a tamper-evident, per-user audit chain you can verify at any time. It runs with zero infrastructure and drops onto Postgres by changing one composition line.

Why it exists

Wiring a prompt to an API key is a demo. Letting a model touch a person's records raises the hard questions. How do you keep it from putting words in someone's mouth? How do you prove your audit log was not edited after the fact? How do you keep every model call zero-retention and least-context, uniformly? How do you keep machine commentary from being mistaken for the user's own words? This repo is a set of answered questions, each written up as an ADR.

Three ideas do most of the work

01

The AI never writes a record

The assistant can only produce a draft or a labeled assessment. A single private commit path is the one door into the record store, reachable only by an explicit human approval - and the AI-facing code has no reference to it. "The AI wrote a record on its own" is not a risk to review for; it is unrepresentable in the code.

02

Every change joins a chain you can verify

Each event's hash folds in the previous one, like a cairn where each stone rests on the last. Alter, drop, or reorder a single event and every hash downstream stops matching, so verification points at exactly where it broke. The same function seals and checks the chain - in TypeScript by default, and in Postgres triggers when a database is used.

03

The model is a guest

One gateway is the only way to reach a model: zero-data-retention on every call, least-context scoping per flow, a transcript written for each, and output that is always a proposal or a clearly labeled artifact. Provider access lives behind a fetch seam, so the whole suite runs offline against a deterministic mock.

From a note to a provable record

One path, with the gate in the middle. Full detail in ARCHITECTURE.md, the audit protocol, and the nine ADRs.

  1. 1

    Your note

    a rough thought - the only input, and the seed of a draft

  2. 2

    AI gateway (single egress)

    zero-data-retention · per-flow model · versioned prompt · transcript

  3. 3

    Draft (a proposal)

    stored, audited - and not a record

  4. 4

    Human review

    read next to the note, edit freely - or reject and commit nothing

  5. 5

    Approve → commitRecord

    the sole writer; stamps provenance and a content hash

  6. 6

    Audit event appended

    to a per-user, tamper-evident chain you can verify anytime

What's inside

Review gate

Draft to review to a single private commitRecord path, reachable only by explicit human approval. The AI paths write drafts and assessments; a test asserts they never change the record count.

Audit chain

A per-user, append-only SHA-256 hash chain with a verifier that shares the writer's exact hash function. Two implementations of one documented protocol: TypeScript by default, Postgres triggers for defense in depth.

AI gateway

The only model egress: zero-data-retention forced on every call, per-flow model routing, least-context scoping, and a transcript for each - so retention and scoping are provably uniform, not per-call hopes.

Provenance

Prompt templates are versioned data, and the version in force is locked onto each record. The audit event stores the record's content hash, not its text, so provenance is verifiable without duplicating content.

Labeled artifacts

AI assessments are stored separately, versioned, and always labeled AI-generated - never merged into the person's own words, so the boundary between the two is preserved in the data model.

Data & security

Row-level security written into each table's migration and enforced by a CI gate, an append-only audit table, GUC-based tenancy, plus gitleaks and an .env.example guard test.

Backends

In-memory by default, so it runs with zero infrastructure and the entire suite is offline. Postgres is a drop-in behind one composition line, with the chain sealed by triggers.

Tooling

CI running typecheck, lint, test, RLS coverage, migration order, build, secret scan, and CodeQL - 50 deterministic offline tests on every push.

By the numbers

50
offline tests
9
architecture decision records
2
implementations of one chain protocol
0
ways for the AI to write a record

The suite is deterministic and offline - the model transport and the store are injected, time and identity are injected, and the Postgres adapters are tested against a scripted executor. The default runtime needs no database and no API key; swapping in Postgres is a change to one composition file.

Explore the code

The repository is fully documented - an architecture overview, an audit-protocol write-up on what the chain proves and does not, nine ADRs, and a diagram.