Homework — break the trigger, try a foreign key
Expand the app. Give your app an append-only trail if it has state worth auditing, or work on SignFlow.
-
Try to rewrite history. In
psql,UPDATEan existing audit row'smessage, andDELETEone. Both are refused by the trigger, with its exact error. Now, as an experiment,DROP TRIGGER audit_events_no_update_delete ON audit_events;and try theUPDATEagain — it works. Put the trigger back. Write two sentences on what this proves about the guarantee's boundary, and what "off-box log shipping" would add. -
Why not a foreign key? Add
REFERENCES documents(id) ON DELETE CASCADEtodocument_id(in a scratch copy of the migration), re-seed, upload and delete a draft, and look ataudit_events. The rows are gone. Now tryON DELETE RESTRICT— the delete is blocked. Explain in your own words why FK-free is the only option that keeps both the delete and the evidence. -
The shared timestamp. Sign the last signer on a document and query
select seq, created_at, event_type from audit_events where document_id='<id>' order by seq. Thesignedandcompletedrows carry the samecreated_at. Explain why (now()in a transaction), and whyseq— notcreated_at— is theORDER BY. -
Frozen vs live. The audit message stores
alice@example.com signed as "Alice"as frozen text. Suppose instead the UI rebuilt that sentence at render time by joining to thesignersrow. Name two ways that could later show something different from what actually happened — and why a frozen sentence can't.
The documents phase is complete: upload, ownership, invitations, signing, and an immutable trail. Next comes deployment — taking all of this to Railway, where the ephemeral-filesystem caveat from the upload lesson finally has to be dealt with.