easterbrooka/mike: Phase 2 crypto wired into live request path with dual-write rollout

easterbrooka flipped user API keys, model settings, and workflow-share records onto envelope-encrypted columns across every read and write path, with a dual-write strategy so rolling back to a previous image doesn't lose data during the soak.

securityinfrastructure

Reads now prefer the ciphertext column and decrypt under the user's data encryption key (DEK), falling back to the legacy plaintext column for rows the backfill hasn't reached yet. Writes go to both: the sealed value lands in the new _ct column and the plaintext column is also written, keeping older images safe to run during the cutover window. The legacy columns stay populated until a follow-up migration drops them.

The shared-workflow lookups were the more involved part. Anywhere the code previously matched on plaintext email - access resolution, the shared-workflows list, the chat tool's workflow store, and the share-creation upsert - now keys off an HMAC index. The upsert's conflict target was updated to match. The share-listing endpoint keeps its response shape but decrypts when the ciphertext column is present.

A one-shot backfill script walks user profiles and workflow shares, mints DEKs as needed, and populates the ciphertext and HMAC columns idempotently against the production Supabase project. For startup safety, production builds now assert that KMS is configured and the email HMAC pepper is present - a misconfigured deploy will fail fast rather than silently using plaintext. A process-wide TenantCrypto singleton amortizes KMS Decrypt calls across requests via an in-process DEK cache; test code continues to instantiate per-call.

All 86 backend tests pass without modification. The existing crypto round-trip coverage and dual-path reads exercise both the ciphertext and plaintext branches.

So what Worth a close look if you're evaluating encryption at rest for API keys and per-user secrets. The dual-write rollout pattern (write both columns, prefer ciphertext on read, drop legacy later) is a low-risk cutover approach. The startup assertion on KMS config is a pattern worth copying regardless - it surfaces misconfiguration before the first real request rather than at the first failure.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?