appEvents is now a HARD DEPENDENCY of two terra user actions: a rejected INSERT rolls back the state change, and the constraints live on a shared table the co-tenant can alter
Surfaced by coder-terra-cc 2026-08-17 while answering a PM question about what logEvent DOES beyond its INSERT. Filed because the consequence outruns TERRA-56 and nobody had written down what the trade costs. THE MECHANISM. src/lib/logger.ts wraps its INSERT in try/catch so LOGGING NEVER CHANGES AN ACTION OUTCOME - a failed event falls back to console and the action still succeeds. CONFIRM_SQL and the new RETIRE_SQL both write appEvents inside the SAME data-modifying CTE as their UPDATE, so they CANNOT do that and MUST NOT: the coupling is the point, since a state change with no record is the erasure gap those features exist to close. Correct design, real behavioural difference from the funnel, not a parity gap. pm accepted it as designed. THE EXPOSURE. Any rejected appEvents INSERT now rolls back a USER-VISIBLE ACTION, not just a log line. appEvents is a SHARED table on a co-tenanted ref and its constraints are not terra to control. Precedent: enamel_031 added a NOT NULL there and raised 23502 on every requestTerraAccess call for nine minutes. Same class, LARGER BLAST RADIUS - that was a failed write, this is a failed user action with an error toast and no row. MEASURED CONSTRAINT SURFACE (db-terra-cc 2026-08-17), and the shape of it is the finding: categoryValue has NO FK and NO vocabulary CHECK, only ck_appEvents_categoryValue_notblank - so an unseeded literal does NOT reject. level and status DO reject: ck_appEvents_level in {info, warn, error, debug} (4 values, no NULL), ck_appEvents_status in {success, failure, denied} (3 values, NULL permitted). THE COLUMN THAT WAS ASKED ABOUT WAS UNCONSTRAINED AND THE REJECTION RISK SAT ON TWO COLUMNS NOBODY ASKED ABOUT. WHAT THIS WI IS FOR, and it is not a redesign: there is NO DETECTOR. Terra has no signal that would fire if enamel tightened an appEvents constraint - the first symptom is a user action failing in production. Establish whether one is buildable from terra side (a cheap periodic assertion over the constraint set terra depends on, compared against a recorded expectation) and what it would cost. A documented no is a legitimate outcome. RELATED SEED FACT worth keeping: appEventCategory has 17 seeded values (auth, authz, session, input, malicious, email, integration, cron, system, navigation, user, toast, client, admin, data, report, client-env). catalogo is ABSENT; dataImport is already in use UNSEEDED. Seeding into lookupOptions is db-enamel-cc work - terra never mutates shared vocabulary rows.