Keep the permanent event record

Fill the night's archive while it is still open, seal it at settlement, and correct it afterwards by appending — because nothing sealed can change.

Owned by Admin · 22 steps · about 26 minutes

Why this exists

Every event ends up with one permanent file: the contract that was actually signed, the counts that were actually taken, the bar and merch money, the sound readings, and a pointer into the ledger for everything financial. It is designed to be readable in ten years without any of the modules that produced it still behaving the same way — which is a much stronger promise than "we kept the data".

The freeze happens at settlement, not at the end of the night

The scheduler flips an event to completed at teardown, typically three or four in the morning. The bar's Z-report, the merch settlement and the promoter's invoice all arrive the following business day. Freezing at completion would therefore guarantee an empty archive, every single time.

So there is a middle state — pending seal — which is fully writable, unbounded, and completely safe to sit in. The automatic seal comes a week after teardown by default. Until then, the record is where you put the numbers.

After the seal, corrections are appends

A sealed record is immutable, and that is enforced by database triggers rather than by policy — the admin data grid and a raw shell obey it too. There is no delete route, no delete function, and no soft-delete column: a permanent record that can be soft-deleted is not permanent.

Corrections are amendments: they name what was wrong, the old value, the new value, a mandatory reason and who did it, and they render underneath the original rather than replacing it. Late real-world movement — a chargeback landing three months later — is drift: recomputed against live sources and appended, never absorbed into the frozen number. The frozen figure and the true figure are both visible, which is the only honest way to show a number that has moved since it was archived.

Money is referenced; documents are copied

The ledger is already append-only and is the platform's financial truth. Two tables holding money would be two answers to one question, so the record stores totals plus a reference into the ledger and writes no amount into any ledger table. Documents are the opposite: the signed contract's bytes are copied verbatim and hash-verified, because the document is the artifact itself and the archive's promise about it is stronger than its source's.

One measurements table, and the retention ladder

There is exactly one table of samples, and a single value is simply a series of length one. Whether a metric is a running total or a stream of deltas is one cell in the metric registry — changing it changes what the headline number means with zero stored rows rewritten.

"Forever" has a cost, so raw samples live for the dispute window, per-minute rollups for seven years, quarter-hour and whole-event rollups permanently — plus the top peak samples, pinned and never deleted at any tier. That last part is the legally meaningful artifact: not "the peak was 103.4 decibels" but "at this instant, this meter read 103.4", kept at full resolution with its provenance for about ten kilobytes.

Before you start

  • An admin session for sealing, amending, the metric registry and devices.
  • A venue_manager or admin session to read a record or enter measurements.
  • An event that has run. The seed ships a completed event with a sealed demo record.
  • The night's paperwork: the bar Z-report, the merch settlement, any sound-meter export.

Practise with

PersonaEmailPasswordNote
adminadmin@club.test admin123the only role that can seal, amend, or manage metrics and devices
venue_managermanager@club.test manager123reads every record and supplies the night's numbers

Steps 1–2 — Admin

their manual →
  1. 1
    The morning after, open the event's record. It already exists — one is opened automatically when the event goes in progress and moved to pending seal when it completes or is cancelled.
    /admin/events/{event_id}/record record
    Expected result A record in pending seal, writable, with the archived documents already captured and the metric tiles mostly empty.
    Watch out for Pending seal is a normal resting state, not a task you are behind on. The automatic seal is a week after teardown by default — that window is the settlement window, and using all of it is fine.
  2. 2
    If a record is genuinely missing — an event that never transitioned normally — open one explicitly. The call is idempotent.
    /api/records/{event_id}/open POST record
    Expected result A record in the open state, or the existing one returned unchanged.
    Watch out for State only ever moves forward, enforced by a trigger. There is no way back from pending seal to open, and none at all from sealed.

Steps 3–5 — Venue Manager

their manual →

Reads every record and is the person who actually has the night's numbers.

  1. 3
    Read the record as it stands. Attendance is already derived from the door scans; the bar, merch and sound tiles are the ones waiting for you.
    /admin/events/{event_id}/record record
    Expected result Frozen counts and ledger-referenced money, plus empty tiles for everything only you can supply.
    Watch out for Scan detail is referenced, not copied — the scanned-in count is frozen as a derived measurement rather than three thousand rows being duplicated so they can be recounted later.
  2. 4
    Type in the numbers you have on paper: bar sales, bar transactions, merch sales, merch units, staff hours, incidents.
    /api/records/{event_id}/measurements POST record
    Expected result Each value stored as a sample with your name on it as provenance.
    Watch out for Money is in integer cents everywhere in this platform. Typing dollars into a cents field is the single most common mistake here, and after the seal it costs an amendment with a written reason.
  3. 5
    For a sound-meter export or a POS report, import the CSV. Run it as a dry run first and read what it says it would accept and reject.
    /api/records/{event_id}/measurements/import POST record
    Expected result A per-row verdict; on the real run, accepted, rejected and duplicate counts.
    Watch out for Rejections are per sample, never per request — one bad timestamp in a thousand rows must not discard the other nine hundred and ninety-nine. Read the rejected list rather than assuming the import failed.

Steps 6–16 — Admin

their manual →
  1. 6
    Look at the metric registry before you invent a metric. It defines the label, the unit, whether the metric is a point or a series, and — the important one — how many samples collapse into one headline number.
    Expected result The catalogue: attendance, bar, merch, sound, ops.
    Watch out for Aggregation is the whole design. A till pushing a running total all night and a till pushing per-period deltas are one registry cell apart, with no stored row rewritten. Get this cell wrong and every headline for that metric is wrong; fix the cell and they are all right again.
  2. 7
    Add a metric the venue actually tracks. Decide its aggregation and its cardinality deliberately, and mark it required at seal only if a record without it is genuinely incomplete.
    /api/record/metrics POST record
    Expected result A new registry row, immediately usable by every ingestion door.
    Watch out for Deactivating a metric never deletes its row, and an unknown key arriving from a device is refused rather than auto-creating a metric. The registry is deliberately something a human curates.
  3. 8
    Open the device list. A metric device is the direct analogue of a door reader: a token, the metric keys it is allowed to post, which event it binds to, and when it was last seen.
    Expected result Registered devices with their last-seen times.
    Watch out for A device that has not been seen since the last event is the thing to notice before the doors open, not after the night is over and the sound data does not exist.
  4. 9
    Register a device and restrict it to the keys it should ever post — a sound meter cannot post bar sales.
    /api/record/devices POST record
    Expected result The device, and its token shown exactly once.
    Watch out for Once is once. If it is not captured at that moment the only way forward is to rotate it, which is a deliberate design and not an inconvenience to work around by storing tokens somewhere convenient.
  5. 10
    Rotate a token when a device is replaced, lost, or its token was pasted somewhere it should not have been.
    /api/record/devices/{device_id}/rotate-token POST record
    Expected result A new token, shown once; the old one stops working immediately.
    Watch out for Rotation is the recovery path for every device credential problem. Budget for reconfiguring the device at the same time — nothing tells it the token changed.
  6. 11
    Understand the device door, because it behaves unlike every other endpoint here: it authenticates on a device token header only, and session cookies are not consulted at all.
    /api/record/ingest POST record
    Expected result Batched samples accepted, with duplicates counted separately.
    Watch out for A logged-in admin posting here without the header is refused, deliberately. A meter is never a person, staff already have a form for typing numbers in, and 'a device can post with a token but not a session' is only a crisp property if there is no fallback. Replayed batches are recognised by their idempotency keys, so a flaky meter retrying cannot double-count.
  7. 12
    Know the one refusal that surprises people: a reading whose timestamp falls inside a night that is already sealed is rejected as sealed, not re-homed onto whatever record happens to be open.
    /api/record/ingest POST record
    Expected result A per-sample rejection naming the reason.
    Watch out for A meter whose clock has drifted by a week must not reopen history, and it must say so rather than quietly filing last month's readings against tonight.
  8. 13
    Before sealing, ask what is blocking. The check names each blocker and which ones cannot be forced.
    /api/records/{event_id}/seal-check record
    Expected result Ready, or a list: orders awaiting payment, open disputes, refunds in flight, a contract that is not yet terminal, a missing required metric, an unbalanced ledger, or a contract whose bytes do not match their hash.
    Watch out for Every blocker is a genuinely unfinished piece of the night. Work the list rather than reaching for force — the list is short precisely so that clearing it is realistic.
  9. 14
    Seal the record. The contract's bytes are copied and hash-verified, the section and signature manifests are captured, the event and venue are snapshotted, and the rollup tiers are materialised inside the same transaction.
    /api/records/{event_id}/seal POST record
    Expected result A sealed, immutable record with its documents attached.
    Watch out for Forcing overrides every blocker except a contract that does not match its own hash — a document that fails its hash is not evidence of anything, and sealing it would launder a corruption into a permanent record. Whatever you force is written into the record's reason and rendered in red on the page forever.
  10. 15
    Correct a sealed record by appending an amendment: what is wrong, the old value, the new value, and why.
    /api/records/{event_id}/amendments POST record
    Expected result The original still rendered, with your amendment beneath it and folded into the comparison view.
    Watch out for The reason is mandatory and an empty one is refused. The point of an amendment is that a stranger can read the pair and understand what happened; a correction with no explanation is just a second number.
  11. 16
    Verify a record whenever you have reason to doubt it — after a restore, after a migration, or before handing it to somebody official.
    /api/records/{event_id}/verify record
    Expected result Stored hashes recomputed and compared.
    Watch out for Verification is about the archive's integrity, not about whether the numbers are right. It answers 'has this been tampered with', which is a different and much more important question.

Steps 17–20 — Venue Manager

their manual →

Reads every record and is the person who actually has the night's numbers.

  1. 17
    Read the sealed record. Post-seal movement appears in its own panel rather than silently changing the frozen figures.
    /admin/events/{event_id}/record record
    Expected result Frozen headlines, a post-seal activity panel where there has been drift, and amendments under their originals.
    Watch out for A drift row is not an error to resolve by editing. Somebody with admin reads it and decides whether it warrants an amendment — that decision is the work, and the drift row is the prompt for it.
  2. 18
    Open an archived document. What you get is the stored bytes, verbatim — the contract exactly as it was signed, not re-rendered from today's template.
    /admin/events/{event_id}/record/documents/{doc_id} record
    Expected result The artifact itself.
    Watch out for Templates get revised and retired. This is why the record also keeps a manifest of which section came from which template version: it proves what was signed long after the template that produced it has moved on.
  3. 19
    Compare nights. Pick the metrics you care about and a date range and read them across events.
    Expected result A comparison table, exportable as CSV.
    Watch out for Comparison reads through the same resolver every other surface uses, so a metric whose aggregation was corrected reads consistently across every night at once — including the ones sealed before the correction.
  4. 20
    For a spreadsheet or a report, take the comparison as JSON or CSV directly.
    Expected result The same numbers the page shows, in a file.
    Watch out for Reading records is a venue_manager job; everything that writes to one is admin, because every write here is permanent by construction and there is no undo anywhere in this module.

Steps 21–22 — Admin

their manual →
  1. 21
    When something does not add up, pull the whole record as one blob: documents with their hashes, riders, facts with their ledger references, resolved metrics, per-tier sample counts, drift, amendments, and the computed record hash next to the stored one.
    /debug/record/{event_id} record
    Expected result Everything about that night on one screen.
    Watch out for The two hashes disagreeing is the strongest signal this module can give you. Treat it as an incident, not as a display bug.
  2. 22
    Sweep the entire archive periodically. An empty list means every record still verifies.
    Expected result An empty list on a healthy install.
    Watch out for Also worth watching: how often seals are forced. If staff routinely force past blockers, the blocker list has stopped being a control and become decoration, and the fix is upstream rather than here.