An email change on users destroys the prior address: userEmails exists, is empty, and has no write path at any layer
MEASURED 2026-09-11 by db-terra-cc at my request, full catalog rows on the row's events. An email change on public.users destroys the prior address: there is storage for address history and NOTHING writes it, at any layer. WHAT EXISTS. public."userEmails" (ordinary table): id, userId, email, emailCanonical, fromAt, toAt, createdAt. fromAt/toAt is a temporal address-history shape - a row with toAt NOT NULL is a superseded address. So the storage half of this is built and correctly designed. WHAT DOES NOT. userEmails holds ZERO rows: total 0, superseded 0, distinct users 0, earliest NULL, latest NULL, against 159 users. public.users carries exactly two non-internal triggers and NEITHER touches userEmails - tg_users_closeGroupsOnDelete (AFTER UPDATE WHEN old.deletedAt IS NULL AND new.deletedAt IS NOT NULL) and trg_users_setUpdatedAt (BEFORE UPDATE). No previousEmail column exists on users, and an ILIKE sweep for previous/prior/former/oldEmail across the WHOLE public schema returns exactly one unrelated hit (terraPacienteAntecedenteChanges.previousDetail). THE LIVE INSTANCE, which is why this is p1 and not a tidiness item. On 2026-09-10 under TERRA-210, db-terra-cc overwrote two REAL client-supplied addresses on users - galvanvictoria97@outlook.com and lauragalvan91@hotmail.com - and both went nowhere. They survive only because that agent wrote them into an evidence file by hand. The next such write, by anyone who does not happen to keep a file, loses the address silently and with no error. WHY THIS IS ITS OWN ROW AND NOT PART OF TERRA-111. 111's blockedReason asserts 'Esquema previousEmail ... disponible', which read as either true or fabricated; it is neither - the precondition is HALF-BUILT, and that third state is invisible from the text. 111 is the user-facing REPLACEMENT FLOW and is blocked on Elazar's design decision. This row is the storage write path underneath it, is blocked on a different person, and would be needed even if 111 were cancelled tomorrow. WHAT IS OWED, and it is smaller than a schema change: NOT a db-enamel-cc column request - the table is already there. What is owed is the write path and its OWNER. userEmails is an enamel-owned relation, so the question for db-enamel-cc is whether the insert belongs to enamel or whether terra may write it alongside its own users email writes. Note the enamel lane has been offline during the PostgREST outage, so expect this to sit. NOT COVERED, stated so nobody reads this as exhaustive: db-terra-cc checked the triggers on users and the table contents, which proves nothing HAS written userEmails; it does not prove no function or view elsewhere COULD. The routine sweep for an outside writer is requested and not yet run.
Activity
-
Blocked on ONE ownership answer from db-enamel-cc, not on a schema change: userEmails already exists, so what is undecided is whether the history INSERT belongs to enamel or whether terra may write it alongside its own users email writes. Nothing here is authorised to start until that is answered - writing to an enamel-owned relation without a GO is the exact class of thing this project forbids. Expect it to sit: the enamel lane has been offline during the PostgREST outage. If it stays unanswered, the escalation is Elazar, and the cost to quote him is concrete rather than theoretical - two real client addresses already went through this hole on 2026-09-10 (TERRA-210) and survive only in an evidence file somebody happened to write by hand. Not blocked on terra code. The write path is a handful of lines once the owner is named.
-
SWEEP RUN - db-terra-cc 2026-09-11, marker UEXSWEEP215-mtws5k. Asked for: prove nothing CAN write userEmails, which is a different claim from nothing HAS. Result: NO WRITER EXISTS AT ANY LAYER I CAN SEE, and the premise of this row holds. (1) DB ROUTINES: swept pg_proc across every schema except pg_catalog and information_schema for prosrc ILIKE '%userEmails%', testing separately for INSERT INTO / UPDATE / DELETE FROM against the table. ZERO ROUTINES MENTION IT AT ALL - empty result set, not merely no writers. Used prosrc rather than pg_get_functiondef because the latter errors with 42809 on aggregate functions and aborts the whole sweep. (2) DEPENDENT OBJECTS: no view or rewrite rule references userEmails. (3) PRIVILEGES, which is the 'could' question: INSERT/UPDATE/DELETE/TRUNCATE granted to postgres and service_role ONLY. Not to anon, not to authenticated. RLS is DISABLED on the table. So the pg pool (which connects as postgres) is fully able to write it - the absence is code, not permission, and no grant change is needed to build the write path. (4) APP LAYER, terra repo: command grep over ts/tsx/sql/js/mjs, node_modules excluded. Exactly ONE file matches and it contains no code - db/migrations/applied/terra_210_terra_auth_question_views.sql, two comment lines, one of which says explicitly 'Does not touch userEmails'. FINDING THAT CHANGES THIS ROW'S BLOCKER, from that same file's header: 'db-enamel-cc GO is NOT required for this file. It IS required for blackboard item 1 (populating userEmails and repointing bindSupabaseAuthId), which is enamel-owned and requested separately.' So the enamel ask this row is blocked on was ALREADY RAISED as blackboard item 1 and documented as enamel-owned - it may be outstanding rather than unmade. Worth checking that blackboard before re-raising, since a duplicate ask to a lane that has been offline is a second wait, not a faster answer. COVERAGE LIMIT, and it is the material one: I swept TERRA'S repo. I cannot grep enamel's codebase from this lane, and userEmails is an enamel-owned relation, so an enamel-side writer would be invisible to every check above except (1) - though (1) covers any DB-resident routine regardless of which app authored it, and the table's 0 rows against 159 users means that whatever exists has never run.
-
THE BLOCK IS NARROWER AND WORSE THAN THIS ROW'S blockedReason SAYS. Marker: ENAMELGONE215-20260911. 1. ELAZAR ALREADY APPROVED THIS WORK, 2026-09-08. It is blackboard item 1 on bb-db-terra-cc-mts3dp5g-b3n9wo - 'Populate userEmails, repoint bindSupabaseAuthId at it' - which I proposed there and ranked HIGHEST VALUE of six, above the canonical views, with the note 'db-enamel-cc owns structure + function, needs his GO'. Elazar's ruling on that board was 'go 1, 2, 3'. terra_210's header records the same split: its own GO was not needed from enamel, but item 1's was, 'requested separately'. So Elazar's authorisation is six days old and is not what this row waits on. 2. THE OWNER WHOSE GO IS REQUIRED DOES NOT EXIST ON THE HUB. roster(db-enamel-cc) returns ZERO rows - not stale, not offline, ABSENT from the registry entirely; the full online listing carries no enamel agent of any role. There is nobody to send the separate request to, and no way to tell whether it was ever sent before the lane went dark. Escalation, not a wait. 3. WHAT THAT MAKES THIS ROW. Not 'blocked on an answer that is coming slowly'. Blocked on a GO that cannot be requested, for work the human already authorised, guarding a hole that has already destroyed two real client addresses (TERRA-210, 2026-09-10). Those three facts belong in one sentence when this goes to Elazar, because each alone reads as tolerable. 4. SCOPE CORRECTION TO THIS ROW'S OWN DESCRIPTION. I filed it as address-history hygiene. Item 1 is bigger: bindSupabaseAuthId resolves on users.emailCanonical ONLY, a single email slot for people who demonstrably have two or three, and populating userEmails plus repointing the bind is the fix for the DOMINANT terra identity failure - the one that made a fully provisioned person (tomas.cardozo, users row since 2026-08-05) report as unprovisioned and unable to bind. Losing the prior address on an overwrite is a SYMPTOM of the same missing object. This row and blackboard item 1 are the same work. 5. STILL NOT STARTING IT. An enamel-owned relation without its owner's GO is the one thing this project forbids outright, and an unreachable owner is not an implied GO. What changes is where this goes next: Elazar, with the roster evidence, not a queue.
-
OWNER NAMED AND HALF THE WORK SUBSTITUTED. Marker: OWNER215-20260911. db-enamel-cc, 2026-09-11: userEmails is enamel's and the history INSERT is enamel's. Created in enamel_001_bootstrap.sql as a shared-domain object; blackboard item 1 said so already ('db-enamel-cc owns structure + function, needs his GO'). TERRA DOES NOT WRITE IT alongside its own users.email writes - that is the per-writer split enamel_045 fixed for updatedAt, and TERRA-210's two hand bindings are the proof of why. Mechanism is an enamel-owned BEFORE UPDATE OF email trigger on users plus a 159-row backfill: a boundary function catches callers only, a trigger catches manual UPDATEs too, and with 0 rows against 159 users a trigger alone would leave every never-changed user with no open row. THE BIND REPOINT IS OFF PERMANENTLY, and this corrects what I asked for. I asked for bindSupabaseAuthId to resolve against ANY of a person's addresses. Measured live: ux_userEmails_current is UNIQUE(userId) WHERE toAt IS NULL - ONE open address per person - and ix_userEmails_canonical is NOT unique. So the ask has no safe reading as built. Open-rows-only is identical to users.emailCanonical and does nothing for tomas.cardozo. Closed-history-rows reintroduces exactly the ambiguity matchCount=1 exists to prevent: a recycled institutional address binds its NEW holder onto the OLD holder's record, which is a wrong-person CLINICAL DATA path. userEmails is a HISTORY table, not an alias table, and I read it as one. pmmaster-evolutiva-cc ruled A, build this week: the history write ships, bindSubabaseAuthId keeps reading users.emailCanonical, and ux_userEmails_current / ux_users_email_canonical_live are not touched. Elazar's 2026-09-08 item 1 is SUBSTITUTED, not deferred - its repoint half cannot be built as worded. Ratified so it is not relitigated. WHAT THIS ROW IS NOW: the data-loss half only, and it is enamel's to build. Terra's interest is that an email correction stops destroying the prior address - under A the history write is what makes ~150 upcoming corrections auditable instead of silent. Option B (N live login addresses) reverses enamel_001 section 11 on a shared table and is Elazar's call, with the weight that B does not remove the recycling risk, it moves it from schema to process. TOMAS.CARDOZO IS THEREFORE STILL UNSOLVED by this row. Whatever fixes his class is not a bind repoint; it is fn_correctUserEmail generalizing TERRA-104, which pmmaster included in the A build.
-
UNBLOCKED IN PRINCIPLE, NOT IN THE CATALOG - and this row is no longer terra's work. Marker: T215-owner-mtwuj2. THE OWNER EXISTS AGAIN. The enamel lane re-registered on the hub at 2026-09-11 10:46Z: coder-enamel-cc and db-enamel-cc, both live and polling, roster read by me rather than taken from the message announcing it. This row's blocker was 'enamel-owned relation, no GO obtainable because there is no lane'. That is discharged - it is an ordinary peer request again. THE SHAPE IS RULED. pmmaster-evolutiva-cc ruled option A: build the history write now - a BEFORE UPDATE OF email trigger on users plus a 159-row backfill, and fn_correctUserEmail generalising TERRA-104. Owner db-enamel-cc, not this lane. Do NOT touch ux_userEmails_current or ux_users_email_canonical_live. AND ELAZAR RATIFIED THE UNDERLYING QUESTION, verbatim 'one': a person has ONE live login address, so switching means the old one stops working. enamel_001 section 11 is therefore a ratified ruling, not a design bet. No isLoginAlias axis, no alias table. A later ask to let both addresses work is a reversal of a live ruling, not a design discussion. THE REPOINT HALF OF THE 2026-09-08 GO WAS SUBSTITUTED, NOT DEFERRED, and the reason is permanent: bindSupabaseAuthId keeps reading users.emailCanonical. Open-rows-only is a no-op by construction; closed-history-rows would bind a recycled UBA address onto the PREVIOUS holder's clinical record, which is a wrong-person clinical-data path. Recording it here because a substitution that is not written down reads as an unexecuted GO six days later - the exact shape of the TERRA-143 refile surfaced this morning. WHAT I MEASURED RATHER THAN ASSUMED, and it is the operative part for anyone reading this row before the work lands. pmmaster relayed that db-enamel-cc's userEmails writer falsifies the premise 'nothing in the database can write it'. Checked at 10:58Z: userEmails holds 0 rows; public.users carries exactly two non-internal triggers, tg_users_closeGroupsOnDelete and trg_users_setUpdatedAt, neither touching it; and a pg_proc sweep across every schema for prosrc ILIKE '%userEmails%' returns the EMPTY SET. So the writer is SPECCED, NOT LIVE. A ruling to build is not a build. Until that trigger exists, overwriting users.email still destroys the prior address permanently and the pre-state must still be recorded in an evidence file by hand. TERRA-215 stays blocked, on a different and much shorter thing: db-enamel-cc shipping the trigger. Terra does not write userEmails and does not need to.
-
SHIPPED AND VERIFIED LIVE - the closing condition of this row is met. Marker: T215-ship-mtwvg9. enamel_048_user_email_history_and_correction.sql applied and pushed by db-enamel-cc at ebb7157, app v0.7.17. I verified the live catalog myself rather than accepting the status message: 159 users / 159 open userEmails rows / 3 closed rows / 0 non-contiguous open rows / every closed row's fromAt equals its user's users.createdAt. The three closed addresses are galvanvictoria97@outlook.com, jordipasart@hotmail.com and lauragalvan91@hotmail.com - the two this row was filed over, plus TERRA-104's, which nobody had counted. public.users now carries tg_users_recordEmailHistory with tgenabled='A' (ENABLE ALWAYS, so session_replication_role cannot bypass it) alongside the two pre-existing triggers, and fn_correctUserEmail exists with proconfig search_path=public, pg_temp intact. IT IS AN AFTER TRIGGER, NOT THE 'BEFORE UPDATE OF email' THIS ROW'S PREVIOUS EVENT NAMED. That changed during review and the reason is load-bearing: a BEFORE trigger records the value as it stands mid-statement, which a later BEFORE trigger on the same table can still change - trg_users_setUpdatedAt is already BEFORE UPDATE on users. AFTER cannot record an address the row does not end up holding. WHAT THIS RETIRES. The evidence-file rule this row exists to compensate for is retired for FUTURE writes: an UPDATE to users.email now writes its own history, including a bare manual UPDATE run by a DB lane with no function call, which is precisely why it is a trigger and not a boundary function. Terra still does not write userEmails and does not need to. WHAT IT DOES NOT RETIRE, stated so this is not read as a clearance. A correction made before 2026-09-01 04:16 that wrote no appEvents row is undetectable from this database, permanently. I claimed on 2026-09-11 that 154 of 159 rows had never been written since creation, derived from count(*) FILTER (WHERE updatedAt > createdAt) = 5; db-enamel-cc falsified it with a named counterexample. trg_users_setUpdatedAt was created by enamel_045 (466d004) at 2026-09-01 04:16, so that predicate detects only writes made after it existed. fe171c44 (Jorge Pasart) was corrected at 04:13:19.550883+00 and still reads updatedAt = createdAt exactly. Four further pre-trigger writes are provable the same way - the four supabaseAuthIdBound events before 04:16, all on rows that read as untouched today. THE DURABLE RULE: before using an updatedAt/modified column as evidence that a row was never written, find the trigger or writer that maintains it and state the date its coverage begins. AND ONE THING I PUT INTO THIS LANE'S EVIDENCE WRONGLY, retracted here because it reached a migration comment before it was caught. I reported appEvents detail.emailCanonicalEqual = false on 4304d643 (Puia) as 'the auth-side address did not match users.emailCanonical at bind time'. It is nothing of the kind. bindSupabaseAuthId builds that field as (lower(btrim(emailArg)) = emailCanonicalArg) where emailCanonicalArg := canonicalizeEmail(emailArg) - it compares an argument to its own canonical form and never reads users at all. canonicalizeEmail strips dots in a gmail local part, so canonicalizeEmail('puia.sebas@gmail.com') = 'puiasebas@gmail.com' and the flag is false, while 'panorexis26@gmail.com' is unchanged and true. The field means 'this address has a dot in a gmail local part'. A jsonb detail key is named by whoever wrote the insert; the only definition is the expression that fills it. Still explained-not-cleared, unchanged: 4304d643, 38c653d5 and a45995ef each hold a single open row at createdAt. A bind fires the updatedAt trigger whether or not the address changed, so their timestamp is explained without an email change but does not prove there was none. Status close is not mine to take unilaterally on a row created on behalf of coder-terra-cc; the measurement is here and the work is done.
-
cleared: Blocked on ONE ownership answer from db-enamel-cc, not on a schema change: userEmails already exists, so what is undecided is whether the history INSERT belongs to enamel or whether terra may write it alongside its own users email writes. Nothing here is authorised to start until that is answered - writing to an enamel-owned relation without a GO is the exact class of thing this project forbids. Expect it to sit: the enamel lane has been offline during the PostgREST outage. If it stays unanswered, the escalation is Elazar, and the cost to quote him is concrete rather than theoretical - two real client addresses already went through this hole on 2026-09-10 (TERRA-210) and survive only in an evidence file somebody happened to write by hand. Not blocked on terra code. The write path is a handful of lines once the owner is named.
-
CLOSED by coder-terra-cc, the lane this row was created on behalf of. Marker: T215-close-mtwvj4. CLOSING CONDITION AS FILED: an email change on users destroys the prior address because userEmails has no write path at any layer. That condition no longer holds. enamel_048_user_email_history_and_correction.sql is applied and pushed by db-enamel-cc at ebb7157, app v0.7.17: tg_users_recordEmailHistory on public.users with tgenabled='A', 159 users / 159 open userEmails rows / 3 closed rows / 0 non-contiguous, fn_correctUserEmail present with its search_path pin. Read off the live catalog by db-terra-cc rather than taken from the shipping report. THE THREE CLOSED ADDRESSES are the two this row was filed over (galvanvictoria97@outlook.com, lauragalvan91@hotmail.com, TERRA-210) plus jordipasart@hotmail.com from TERRA-104, which nobody had counted when the row was written. AFTER, NOT THE 'BEFORE UPDATE OF email' THIS ROW'S OWN EVENTS NAMED, and the change is load-bearing rather than incidental: a BEFORE trigger records a value a later BEFORE trigger on the same table can still change, and trg_users_setUpdatedAt is already BEFORE UPDATE on users. Anyone reading this row for the mechanism should take the AFTER shape, not the earlier events. WHAT IS RETIRED: the hand-written-evidence-file rule this row existed to compensate for, for FUTURE email writes only. A bare manual UPDATE on users.email by a DB lane now records its own history - which is why it is a trigger and not a boundary function. Terra writes nothing here and does not need to. WHAT IS NOT RETIRED, so this close is not read as a clearance: a correction made before 2026-09-01 04:16 that wrote no appEvents row is undetectable from this database, permanently. Two of db-terra-cc's own claims that reached other lanes' artifacts are retracted on the preceding event under marker T215-ship-mtwvg9 - '154 of 159 rows never written since creation' (false; trg_users_setUpdatedAt only exists from 2026-09-01 04:16, fe171c44 is a proven miss) and detail.emailCanonicalEqual as an auth-vs-users comparison (it compares an argument to its own canonical form; false means only 'a dot in a gmail local part'). THE BIND REPOINT IS NOT PART OF THIS CLOSE. Elazar's 2026-09-08 item 1 was SUBSTITUTED, not delivered: bindSupabaseAuthId still reads users.emailCanonical, ux_userEmails_current stays UNIQUE(userId) WHERE toAt IS NULL, and Elazar ratified 'one' live login address on 2026-09-11. tomas.cardozo's class is fixed by fn_correctUserEmail, never by resolving a bind against history rows. Note wi unblock cleared blockedReason on the 11:28 event as usual; the gate and its residual live in that event body, not in the field.