Work log — 2026-03-26
What shipped today
Today’s session was a focused security and reliability hardening sprint — four issues in the same thematic cluster, all shipped. The thread running through all of it: silent failures and secret exposure paths that had been baked into the codebase since early development.
Fail-loudly hardening (#488) tackled the settings and data-export endpoints, which had been swallowing errors and returning partial success. getFeedbackLoopHealth() now treats any query failure as a hard error captured to Sentry. The export route no longer returns a partial ZIP with an inline warning — it returns a 500 and the ZIP never leaves the server. A companion test file (settings-errors.test.ts) covers the five main error scenarios, including the subtle distinction between PGRST116 “not found” (expected, never sent to Sentry) and genuine database errors.
Passkey ceremony hardening (#489) addressed the four passkey route handlers — options and verify for both register and login. Challenge inserts, credential lookups, counter updates, and challenge cleanup all now fail loudly with appropriate Sentry capture. The critical detail: cleanup failure blocks session issuance rather than being swallowed silently. New passkey-routes.test.ts covers eight scenarios including the PGRST116/real-error distinction that determines whether a missing challenge is a normal expiry or a database outage.
Secret exposure closure (#490) was a two-pronged fix. The user_profiles_decrypted view had been returning raw api_key plaintext and the full integrations_encrypted blob over the wire to every authenticated read. A migration drops and recreates the view with has_api_key (boolean) and integrations (decrypted JSON), removing the raw values entirely. Separately, the public RSS feed URL was using a loose LIKE pattern to match newsletter addresses — tightened to exact match with a stricter 12-char hex regex. The view rebuild required DROP + CREATE because PostgreSQL won’t remove columns via CREATE OR REPLACE VIEW; grants were re-added after CASCADE cleared them.
Admin impersonation guard (#491) wires the _realAdminId sentinel (already set by getUser() during impersonation sessions) into the deleteAccount() action. The guard returns a clear error message before any destructive work begins. getSettings() now surfaces isImpersonating as a boolean, and the settings page DangerZone hides the delete button entirely when the flag is set, replacing it with an explanatory notice. Two regression tests confirm the guard fires and that normal paths are unblocked.
Completed
- #488 — Settings and export endpoints should fail loudly instead of returning partial success
- #489 — Passkey routes ignore challenge persistence and cleanup failures
- #490 — Close secret exposure paths in decrypted profile reads and public feed URLs
- #491 — Block destructive account actions while admin impersonation is active
Release progress
No open milestones. All four M1–M4 milestones closed. The open queue is a backlog of test coverage issues and a few UI/UX bugs, none milestone-tracked.
Carry-over
Test coverage issues remain in the ready-for-dev queue: handler tests for user.learn (#502), newsletter.process (#501), briefing.generate (#500), article mutations (#499), and billing webhooks (#498). These are mechanical test-writing work rather than product issues.
Three prep-needed issues are also queued: #508 (fix Stripe webhook mock — already partially resolved by work done in #488’s tests), #505 (push handler network-error tests), and #504 (briefing.generate fixture alignment).
Risks
The migration removing api_key from the decrypted view (#490) will break any code that reads profile.api_key from the view. The web app was already updated to use hasApiKey, but any other consumers (engine, admin tooling) need to be checked. The view is accessed via the user_profiles_decrypted view in authenticated reads — worth scanning for remaining api_key references.
Flags and watch-outs
supabase db pushwas not run for the #490 migration during this session — it was created and committed but not pushed to the remote database. Runsupabase db pushbefore the next deploy or any auth-related testing.- The Stripe webhook mock inconsistency (#508) was already partially resolved by the test work in #488, but the issue remains open for proper triage.
- PostHog and Sentry env var verification remains a carry-over from prior sessions (noted in memory).
Next session
- Run
supabase db pushto apply theuser_profiles_decryptedview migration — this is blocking any deploy that touches profile reads. - Scan the codebase for remaining
api_keyreads againstuser_profiles_decryptedthat weren’t caught in the web app update. - Pick up
/issue next --autoon the ready-for-dev test coverage queue (#498–#502). - Triage #508 (Stripe webhook mock) — likely a quick close since the fix was already applied.
Why customer tools are organized wrong
This article reveals a fundamental flaw in how customer support tools are designed—organizing by interaction type instead of by customer—and explains why this fragmentation wastes time and obscures the full picture you need to help users effectively.
Infrastructure shapes thought
The tools you build determine what kinds of thinking become possible. On infrastructure, friction, and building deliberately for thought rather than just throughput.
Server-side dashboard architecture: Why moving data fetching off the browser changes everything
How choosing server-side rendering solved security, CORS, and credential management problems I didn't know I had.
The work of being available now
A book on AI, judgment, and staying human at work.
The practice of work in progress
Practical essays on how work actually gets done.
Designed to learn, built to ignore
The most dangerous organizational failures don't throw errors. They look fine, return results, and quietly stay frozen at the moment of their creation.
The variable that was never wired in
The gap between having a solution and using a solution is one of the most persistent failure modes in organizations. You see the escaped variable. You see the risk register. You assume the work is done.
Your empty queue isn't a problem
Dropping a column from a production database is the organizational equivalent of admitting you were wrong. Five projects cleared their queues on the same day, and the bottleneck that emerged wasn't execution — it was taste.