---
title: Fresh-install starting point (why a new install does not replay the migration ladder)
---

# Fresh-install starting point (why a new install does not replay the migration ladder)

## What it is

When Omniscio starts and finds no database, it has to create one — 672 tables, 1,489 indexes and 847 triggers, plus the handful of rows the migrations seed. Until 2026-09-28 it did that the only way it could: by running every migration in order. That is about 1,170 steps and **measured at roughly 9.2 seconds on a fresh machine**, against 362 ms when the database already exists. Every new user paid it once, on their very first launch.

Now a new install copies a ready-made database into place instead. The ladder still runs — it simply finds nothing left to do, and finishes in milliseconds. The wall a user sees is a file copy.

## Why it is safe

The whole promise rests on one claim: **the image is exactly what a full replay would have produced.** Three things hold that up.

- **The image is generated by running the real ladder**, not by transcribing it. `npm run db:build-fresh-start` runs the same `runMigrations` the app calls, on an empty in-memory database, and serializes the result. A generated SQL script was rejected for this reason: reproducing FTS5 shadow tables, virtual tables, views and blobs by hand is where a wrong schema would come from, and there would be no way to be sure.
- **A test compares the two, every build.** One database is created from the committed image, another by a full replay, and they must agree on every schema object, the applied-migration set, the recorded versions and every table's content. That test is what turns "it should be identical" into something the build enforces.
- **The image carries its own record of what it contains.** Its `applied_migrations` table *is* its manifest, so there is no second file that could drift out of step with it.

## The rule it follows

The image may stand in for the ladder **only while it can prove it is a prefix of the migrations this build ships**:

- every shipped migration at or below the image's newest entry is already applied in the image; and
- the image holds no applied migration this build does not have.

Anything else is a refusal, and refusing is always correct — it means building the database the ordinary way, which is exactly what happened before this feature existed. The cases that land there:

- a migration that **landed late carrying an older timestamp** — it sorts below the image's newest entry, so the image appears to cover it while never having applied it;
- a migration that was **deleted** after the image was built;
- a **truncated, foreign or missing** image.

Each refusal is logged with its reason, so a shortcut that quietly stopped firing can never look healthy.

## What it deliberately does not do

- **It never touches an existing install.** A database file that already exists is not read, replaced or re-stamped; that install applies its pending migrations exactly as it always has.
- **It does not unfreeze anything.** The numbered baseline and the target version are untouched, and a migration that lands after the image is simply pending — the ordinary runner applies it on top.
- **It carries the seeded rows, not just the structure.** Only 14 tables hold real rows in a fresh database — the default Tasks inbox list, the saved-prompt catalogue, a few singleton settings — and a schema-only starting point would have left every new user without them.
- **It does not spend a pre-migration snapshot.** That snapshot exists to roll an upgrade back; a database being created has nothing to roll back to.

## Where it bites

- **A stale image is a slow install, not a wrong one.** If the image stops matching the ladder, the app declines it and pays the seconds it was meant to save. The build guard in `tests/integration/fresh-start-equivalence.test.ts` is what stops that reaching users in the first place.
- **The image is a committed binary.** It is regenerated by `npm run db:build-fresh-start`, and editing it by hand is a defect — exactly like hand-editing a migration's timestamp.
- **One residual, stated rather than hidden.** A migration written *later* that depends on a seeded row's exact value (a hard-coded id, say) could behave differently on a fresh install than on an upgraded one. The comparison test covers structure and normalised content, so it would not catch that; it is mitigated by the "a later migration still runs on top" test and the empty-folder boot in `tests/perf/boot-trace.spec.ts`, not eliminated.

## Where to look

- The rule: `.claude/memory/contracts/migration-ledger-contract.md`, `a-fresh-install-may-start-from-a-generated-image`.
- The generator: `scripts/build-fresh-start-schema.ts`, run by `npm run db:build-fresh-start`.
- The runtime decision and the copy: `src/main/db/fresh-start-schema.ts`.
- The committed image: `resources/fresh-start/fresh-start-schema.sqlite`.
