Two files, and only one of them is true
Sizzle keeps its whole record in one data root, a directory set aside for the engine's data and kept apart from its code. The family registry names that root and backend/settings.py reads it, so a test or a second engine can point somewhere else with SIZZLE_DATA_DIR. Inside it, four things live side by side:
| Path | What it holds | If it disappeared |
|---|---|---|
journal.jsonl | One JSON event per line, appended and never rewritten | The record is gone |
mirror.sqlite3 | The tables every route reads | The next start builds it again |
photos/ | Each photo's original bytes and two derived JPEGs | Plates keep their events, and the photo route answers 404 |
staged/ | Photos sent ahead of the plate that will claim them | A plate naming them is refused until they are sent again |
The house sign puts the rule in one line: the journal is the record, and the mirror is disposable. Every durable fact is an event in the journal first. The SQLite file is a view of those events that the API can query quickly. This track is about that split, and about the small amount of code that keeps it honest.
One write path
There is exactly one function in the engine that changes the record: Sizzle.record in backend/domain.py. It takes an event type and its data, appends the event to the journal, applies the same event to the mirror, and returns it. A capture, a reading, a class proposal, Dad's correction, an adoption and every turn of a conversation all pass through it. The house sign lists it as invariant 1 and gives the grep that proves it: rg 'record\("' backend finds every writer.
The lock around it is a re-entrant threading.RLock. That detail matters in lesson 4. capture takes the same lock to re-check a request id and then calls record while still holding it. A plain lock would deadlock right there.
What one append does
Journal.append builds the envelope that every event shares: a ULID id, the dotted type, at as UTC with milliseconds and a trailing Z, and the event's own data. It serialises that with compact separators, so one event is one line, and with ensure_ascii=False, so a Korean principle name stays readable when you open the file. Then, under the journal's own lock, it opens the file in append mode, writes the line and a newline, flushes Python's buffer and calls os.fsync. Only after that call comes back does append return.
An fsync on every event sounds expensive, and in a chat log streaming tokens it would be. Sizzle's events are human-paced: a plate, a reading, a button Dad pressed. Paying for an fsync on each one costs almost nothing here. What it buys is narrower than it sounds, though. The engine runs on macOS, and Apple's fsync man page says the call moves the data to the drive, while the drive may keep it in its own cache and write it out later, even out of order. Only the F_FULLFSYNC fcntl asks the drive to flush that cache, and the journal does not use it. So a returned append means the operating system handed the bytes to the drive. It does not promise they survive a sudden power cut, or that lines reach storage in the order they were written. For a record of plates that is a fair trade, as long as you know it is one.
Why the journal goes first
Look at the order inside record. If the process dies after the append and before the apply, the event is in the journal and the mirror never saw it. The next start replays the journal, so the event appears then. Nothing is lost. Reverse the order and the same crash leaves a mirror row with no event behind it. The next start deletes the mirror and replays the journal, and the change Dad already saw quietly disappears.
Journal-first has one consequence you have to design for. Once an event is in the journal, every future start will apply it again. So the journal should only ever receive events the mirror can accept. That is why the checks live before record: DomainError for an unknown plate kind, a capture with no photos, a class that does not exist, a correction with no value. Lesson 4 shows what one event the mirror refuses does to every later start.
Never edited, never removed
The journal's docstring says it plainly: an event is never edited or removed. A correction is a new correction.added, an adoption is a new class.adopted, and a re-read is a new reading.recorded. Nothing in the engine deletes an event, and there is no route that could. That has an everyday cost the house sign names: a test plate belongs in a test engine. Live checks run with SIZZLE_DATA_DIR pointed at a scratch directory and SIZZLE_PORT=7690, because a plate captured into Dad's journal stays there.
Where this shape comes from
Sizzle did not invent the split. Its architecture doc lists Forge, Vesta and Waystone as engines with the same shape: an append-only JSONL journal as ground truth and a derived SQLite mirror recovered by purge and replay. cwkPippa's own chat record follows the same rule, and pippa-quest's track on JSONL ground truth tells that story from Pippa's side: Dad never sees something the JSONL does not already hold.
The mirror itself is plain Python sqlite3: one connection, sqlite3.Row as the row factory, check_same_thread=False behind the store's own lock. If those words are new, sqlite-quest's Python sqlite3 track covers each one, and its Writes, Transactions, and WAL track covers what a commit actually promises. This track uses SQLite as a fast cache that can be rebuilt at will.