Integrity seals and guarded restart
Tamper-evident seals over the record, one write chokepoint that refuses until integrity is proven, and a guarded restart that passes the same verification gate as a crash: deliberate maintenance gets no privileged path.
Every claim on this page checked against the product source at a pinned revision: 2026-08-19 @ 0c2a08a7.
What this is
The daemon holds your working record: entities, documents, knowledge, the financial ledger, and the mutation journal that makes every write reversible. This page covers the guarantee wrapped around that record’s lifecycle. The record carries authenticated integrity seals. The daemon verifies those seals before it admits writes. A daemon that cannot prove its record intact holds writes and says so, in a structured error naming the exact recovery step, rather than silently continuing. And restart is a guarded operation: deliberate maintenance passes the same verification gate as a crash, because there is no privileged path around it.
Time Machine covers what the mutation journal records and how a write is reverted. This page is about sealing and lifecycle: how the daemon knows the journal and the tables it protects were not altered behind its back, and what it does when it cannot know.
Why it exists
A local-first store has a failure class that hosted products outsource to their ops team: nothing stands between the database file and any process running as your OS user. The realistic threat is rarely an attacker. It is a crashed capture, a second daemon instance, a well-meaning script opening the database directly, or a restore from the wrong backup. In every one of those, the worst outcome is not the damage itself; it is a daemon that keeps writing on top of it, compounding a recoverable state into an illegible one. So the design rule is the same one the rest of this system follows: the daemon must be able to prove the record it is about to extend is the record it last attested, and when it cannot, refusing loudly beats proceeding quietly.
How it works
What is sealed. The integrity model is three composing mechanisms, stated
in the module’s own header (proxy/src/backend/integrity.ts:1-11): a boot-time
integrity seal that detects out-of-band database modifications, journal gap
detection that finds rows modified without a journal entry, and an HMAC-chained
mutation journal that makes the journal itself tamper-evident. The seal is a
file (.integrity-seal, beside the database in the data directory) written at
clean shutdown, before the database closes. It records the database size and
mtime, per-table row counts, the last mutation ids, and a cryptographic state
root per critical table, all covered by one HMAC. The critical tables are the
six that carry your record: the mutation journal, knowledge, document
artifacts, entities, financial transactions, and the wiki page index
(CRITICAL_TABLES, proxy/src/backend/integrity.ts:436). Every journaled
mutation additionally carries a chain_hash linking it to its predecessor, so
the journal reads as one authenticated chain rather than a pile of rows.
One key, owner-held, no fallback. Seals and the journal chain are
authenticated with a dedicated signing key: a 256-bit secret in a file readable
only by your OS user (.journal-integrity-key in the data directory,
proxy/src/config/journalIntegrityKey.ts). The module’s header states the
policy: the key deliberately has no environment, grant-key, session-key, or
public fallback, because losing or replacing it makes an existing chain
unverifiable and therefore read-only, and “silently minting a replacement would
bless unknown history.” Every read of the key re-validates that it is a regular
file, mode 600, owned by the current user, and unchanged while being read.
Checkpoint manifests are signed with a separate key held to the same file
discipline (ensureCheckpointSigningKey,
proxy/src/integrityRecovery/checkpoint.ts).
Verification happens before writes, at one chokepoint. Every
subscriber-data mutation flows through a single gate,
ensureMutationJournalWritable (proxy/src/backend/integrity.ts:9297), which
refuses while integrity authority is anything other than proven. On boot, the
daemon publishes ready quickly but holds writes while it verifies the seal
against the current corpus, because a whole-corpus check does not fit the
startup budget and a single write landing mid-verification would make the
verification meaningless. The source calls this hold “load-bearing, not
conservatism” (proxy/src/runtime/integrityAuthority.ts). The verification
itself runs in an isolated worker process under its own systemd unit, with a
memory ceiling and a ten-minute wall clock, and its transcript is
authenticated back to the daemon; the only rollback is an explicit operator
selection of the previous in-daemon scan, and an unrecognized rollback value
refuses startup instead of weakening the path
(proxy/src/daemon/integrityVerifierSupervisor.ts).
Refusal is a state, not a crash. The two refusals are deliberately
different. During the boot verification window, a write gets a transient error
that says in its own text that no operator action is required; the source
describes it as “an ordinary, self-clearing” not-yet. A broken seal whose
underlying data still verifies clean (SQLite integrity check passing, journal
chain fully verified read-only) boots the daemon into read-only degraded mode:
HTTP and MCP stay up, reads are served, and every mutation is refused with a
structured error carrying the discrepancy categories, the evidence that the
data itself verified, and the exact recovery command, sophia integrity resume-authenticated-tail (IntegrityDegradedWriteRefusedError,
proxy/src/runtime/integrityAuthority.ts). A daemon in trouble is visible and
queryable, not gone. sophia integrity status renders the daemon’s own
published state and computes nothing itself, so the CLI cannot drift into a
second, subtly different definition of the same state
(proxy/src/integrityRecovery/cli.ts).
What a restart looks like
Restarting the daemon is not kill and hope. It is a receipted ceremony the
CLI drives end to end (guarded-restart, proxy/src/integrityRecovery/cli.ts):
sophia integrity guarded-restart
# 1. Target proof systemctl show names the exact unit and binary this
# command is about to restart; a mismatch refuses here,
# before anything stops.
# 2. Prepare the running daemon quiesces, checks whether a scheduled
# background checkpoint inside the coverage window already
# carries the rollback contract, captures one if not, and
# mints a prepare receipt with an expiry. Preparation is a
# durable server-side job: the CLI polls a job id against
# a deadline, so a slow capture survives a dropped
# connection.
# 3. Guarded close the daemon drains in-flight requests, writes a fresh
# integrity seal over the corpus, and exits cleanly.
# 4. Offline gate with the stop proven through systemd, the CLI takes the
# offline ownership lease (one flock through the canonical
# lockfile), the only step that proves no other process
# still holds the data, and checks the stopped files match
# the receipt.
# 5. Start, verdict systemctl start, then the CLI polls /readyz and reports
# the daemon's own published state: "normal",
# "integrity_degraded", or "start_attempted_state_unknown"
# when it cannot honestly claim either. Two properties of this flow are worth naming. First, cost: the governing
decision (docs/decisions/2026-07-29-restart-architecture-wal-plus-background-snapshot.md)
binds restart cost to changes since the last snapshot, never to total database
size. When background checkpoint coverage is fresh, the restart “writes the
seal, witnesses the journal head, and stops”
(proxy/src/integrityRecovery/restartCoverage.ts); anything uncertain about
that coverage resolves to taking a capture, never to skipping one.
Second, refusal behavior. A guarded restart that refuses partway keeps its refusal: the command exits non-zero with the original error intact. But it still brings the daemon back, because a record the restart would not vouch for boots read-only degraded and visible, and per the source, “An absent state platform is the worse failure; a degraded one is legible and recoverable.” Offline recovery ceremonies carry the same discipline: each one takes explicit expected digests on the command line (the manifest hash, the proof digest, the old seal hash) and explicit acknowledgment flags, publishes an intent sentinel before changing anything, and produces a receipt, so an interrupted recovery is resumed or explicitly abandoned, never silently retried into a different operation.
And the gate is symmetric: a crash and a deliberate restart converge on the same boot verification. Nothing about being intentional buys a way around the seal check. The guarded flow exists to make deliberate restarts cheap, single-owner-proven, and receipted, not to bypass anything.
Boundaries
This is tamper evidence, not tamper proofing. The signing key lives on the same disk, readable by the same OS user, as the database it authenticates. A capable process running as you could rewrite the record and mint a fresh seal over it. What the seal actually defends against is the common and corrosive case: accidental out-of-band writes, partial restores, crashed captures, and a second writer, each of which breaks the seal or the chain and surfaces as a named discrepancy instead of silent divergence.
Checkpoints are local, authenticated backups: signed manifests, verified restores, retention. They live on the same machine, so they are not disaster recovery for a lost disk; that remains your backup strategy.
This page also inherits The Security Model’s local-trust boundary. Whoever controls your OS user controls the daemon, its keys, and its database. Integrity seals make interference with your record legible; they do not defend against a person who already has your account.