Commit d024ffc
fix(audit): stop silent audit-capture loss — self-heal triggers on every boot (#467)
* fix(audit): keep audit worker UPDATE/DELETE on DataChangeLog grant
apply-triggers.ts revoked UPDATE/DELETE on DataChangeLog from CURRENT_USER
on the premise that the connecting role always owns the table (where a
REVOKE is a no-op against the owner's implicit rights). In production the
runtime role is not the table owner, so the REVOKE actually stripped the
audit worker's UPDATE/DELETE — silently stalling the CDC processed-cursor
advance and the retention purge.
Converge on a working grant set on every run instead: GRANT
INSERT/SELECT/UPDATE/DELETE to the connecting role and REVOKE UPDATE/DELETE
from PUBLIC only. The append-only invariant is unchanged — the BEFORE
UPDATE/DELETE enforcement triggers reject every non-cursor mutation
regardless of grant state, so they remain the real guarantee. Add an
end-of-run has_table_privilege self-check that fails loudly if the
connecting role lacks any of INSERT/SELECT/UPDATE/DELETE, so a future
self-revoke regression breaks the deploy instead of stalling silently.
This makes apply-triggers idempotent and safe to re-run on every deploy
and restart.
* feat(audit): self-install audit triggers on every app boot (launch-agnostic)
`prisma db push` silently drops the audit triggers, and not every launch path
re-runs apply-triggers: the docker entrypoint does, but a bare `node server.js`,
`next start`, pm2, or a k8s `command:` override that runs `prisma db push` alone
does NOT — causing silent audit-capture loss with no error (exactly what bit prod).
Make the trigger substrate self-healing from the app's own startup, so it holds
no matter how the app is installed, updated, or launched:
- scripts/apply-triggers.ts: extract the apply logic into an importable
`applyAuditTriggers()` (advisory-locked so concurrent replica boots can't deadlock
on DROP/CREATE TRIGGER; injectable logger; bundle-safe SQL path resolution). CLI
behavior preserved and now import-safe via a `require.main === module` guard.
- lib/audit/ensureAuditTriggers.ts: once-per-process, fail-open boot helper
(DIRECT_DATABASE_URL preferred; AUDIT_TRIGGER_BOOTSTRAP_FATAL=1 to fail-closed for
the worker tier; AUDIT_TRIGGER_BOOTSTRAP=off to skip).
- instrumentation.ts: call it on server boot (nodejs runtime). Next's instrumentation
runs on every launcher, making this the one launch-agnostic install point.
- next.config.ts: externalize `pg`; trace prisma/audit_row_change.sql into standalone.
- package.json: add canonical `db:push` / `db:push:dev` (db push + apply-triggers) so
the explicit install/update path can't run `prisma db push` alone.
Builds on the grant fix in fix/datachangelog-append-only-grant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(audit): self-install audit triggers from the worker tier too
The boot-time self-install was wired only into Next's instrumentation hook, which
runs in the web tier. Workers are separate pm2 processes that don't run that hook,
so a worker-only boot — or a web pod that fails its startup asserts before the
install — could leave the audit-log worker draining a DataChangeLog whose capture
triggers were silently dropped by `prisma db push`.
Re-attach the substrate from the audit-log worker's own boot. It's the natural
owner: the direct consumer of the capture triggers, so its correctness depends on
them existing. The call sits in the standalone (require.main) boot path before the
worker starts consuming, reusing the same idempotent, advisory-locked
applyAuditTriggers(). Fail-open by default so a DDL hiccup can't crash-loop the
worker; AUDIT_TRIGGER_BOOTSTRAP_FATAL=1 makes it refuse to start without the
substrate.
Also refresh the apply-triggers.ts header to document the three re-attach points
(schema sync, deploy entrypoint, and launch-agnostic app boot).
* fix(audit): only self-install triggers from the worker in single-tenant mode
The worker-tier bootstrap connected to DIRECT_DATABASE_URL ?? DATABASE_URL on boot,
but in multi-tenant mode the audit-log worker has no single database of its own — its
DATABASE_URL is a placeholder and it resolves a connection per tenant per Loop B cycle.
So the bootstrap would connect to the placeholder, fail, and (fail-open) log an error on
every worker boot while installing nothing. Tenant trigger install in multi-tenant
deployments is owned by each tenant's web app via the instrumentation hook.
Gate the call on !isMultiTenantMode() (already imported and used throughout the worker):
single-tenant installs self-install as intended; multi-tenant workers skip cleanly with a
log line. Robust by construction — no per-deployment AUDIT_TRIGGER_BOOTSTRAP=off needed,
so a future multitenant-workers deploy can't drift into the noisy/futile path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(audit): clarify boot-time audit-trigger self-install for installers
Document that prisma db push drops the audit-capture triggers but the app
re-installs them on every boot, so external-database installers need no
separate trigger step; document the AUDIT_TRIGGER_BOOTSTRAP and
AUDIT_TRIGGER_BOOTSTRAP_FATAL knobs. Fix the DataChangeLog append-only
trigger name in background-processes (tpl_dcl_no_delete/tpl_dcl_no_update).
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>1 parent 7a075d2 commit d024ffc
8 files changed
Lines changed: 276 additions & 43 deletions
File tree
- docs/docs
- testplanit
- lib/audit
- scripts
- workers
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
142 | 142 | | |
143 | 143 | | |
144 | 144 | | |
145 | | - | |
| 145 | + | |
146 | 146 | | |
147 | 147 | | |
148 | 148 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
266 | 266 | | |
267 | 267 | | |
268 | 268 | | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
269 | 283 | | |
270 | 284 | | |
271 | 285 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
4 | 7 | | |
5 | 8 | | |
6 | 9 | | |
| |||
22 | 25 | | |
23 | 26 | | |
24 | 27 | | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
25 | 37 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
131 | 131 | | |
132 | 132 | | |
133 | 133 | | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
134 | 137 | | |
135 | 138 | | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
136 | 144 | | |
137 | 145 | | |
138 | 146 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
| |||
0 commit comments