Buffer-to-hex conversion fixes bytea writes through PostgREST

When easterbrooka's envelope encryption started writing ciphertext columns, the first production insert would have 500'd. `JSON.stringify` serialises a Node Buffer as `{"type":"Buffer","data":[...]}`, which PostgREST cannot coerce to bytea. This commit fixes that before any data hits the wire.

securityinfrastructure

PostgREST expects bytea values in \xdeadbeef hex-literal format. The supabase-js client serialises request bodies with JSON.stringify, so a raw Buffer reaching an insert or upsert call becomes an object literal in the JSON payload - PostgREST rejects it. Equality filters on hashed email columns would have had the same problem: the Buffer stringified to garbage in the URL query string rather than a hex literal. The read path was already working; PostgREST returns bytea as hex-prefixed strings and the existing decoder handled those correctly.

The fix is a toHex helper that converts a Buffer to the \x... format PostgREST expects, and the discipline of calling it at every insert, update, upsert, or equality filter that touches a bytea column. Call sites span DEK provisioning, the user dual-write path, the workflow share upsert and its lookup filters, the chat tool's workflow store lookup, and the envelope backfill script.

The test suite was updated to enforce the contract. The fake Supabase used in unit tests now rejects raw Buffers on insert with the same error real PostgREST would surface, so any new call site that skips the conversion trips a test immediately. A round-trip test covers the encode/decode pair. There's also an explicit test demonstrating the JSON.stringify failure mode - it reads as both a regression guard and inline documentation. Backend tests moved from 88 to 90 passing.

So what Required if you're adopting easterbrooka's envelope encryption work - without this, the first encrypted write to Supabase fails in production. The fake-Supabase enforcement pattern is also worth noting: building the failure mode into the test double means future contributors can't accidentally reintroduce the bug by adding a new write path without the conversion.

View this fork on GitHub →

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