Homework

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.

  1. Try to rewrite history. In psql, UPDATE an existing audit row's message, and DELETE one. 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 the UPDATE again — 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.

  2. Why not a foreign key? Add REFERENCES documents(id) ON DELETE CASCADE to document_id (in a scratch copy of the migration), re-seed, upload and delete a draft, and look at audit_events. The rows are gone. Now try ON 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.

  3. 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. The signed and completed rows carry the same created_at. Explain why (now() in a transaction), and why seq — not created_at — is the ORDER BY.

  4. 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 the signers row. 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.