Field encryption
Per-tenant data keys in Supabase Vault that encrypt app table columns and custom fields at rest, with decryption only through definer functions that check scope and can audit each read.
field-encryption stores selected values as ciphertext. Each owner (a
tenant, a user, or the app for tables without a scope) gets one random data
key in Supabase Vault, and
pgcrypto encrypts the value with AES-256 before the row is written. The key
never leaves the database, and nothing in your config or repository holds key
material.
better-supabase add field-encryptionInstall it, then mark what to encrypt:
- an app table column with
encrypted: true, read back throughpublic.<table>_reveal; - a custom field with
encryptDefinition(id), read back throughreadValues.
Set auditReads on either to write a read entry to the
audit log each time a value is decrypted.
What is stored
A value is stored as bsf1: followed by the base64 of the pgcrypto message.
The data key is a Vault secret named bs:field-key:<owner>, where the owner
is tenant:<id>, user:<id> or app. A key is created on the first write
of its owner and is never rotated in place.
| Function | Who | What it does |
|---|---|---|
field_encrypt(owner, value) | service role | Encrypts a value with the owner's key, creating the key |
field_decrypt(owner, value) | service role | Decrypts a value; null when the key was dropped |
drop_field_key(owner) | service role | Deletes the owner's key, which shreds every value |
field_key, field_seal, field_open | nobody through the API | The helpers the definer functions call |
Signed-in users can't call any of them. They read plaintext only through the definer functions of the table or block that owns the value, which check the caller's scope first.
Shredding
drop_field_key('tenant:<id>') deletes the tenant's key from Vault. Every
value sealed under it then reads as null, in the database and in every
backup taken after the key's Vault row is gone, so dropping the key is how
you erase a tenant's encrypted data without rewriting rows. It records a
field_key.dropped event and a security audit entry with the owner, never
the key.
Limits
- There is no searchable equality. The database only sees ciphertext, so an
encrypted column can't be part of a unique key, an index, a reference, a
search column, a subject title or URL, a rollup or a lookup.
Search and compare the values after
revealin server code, or keep a separate non-sensitive column for lookups. text[]columns, identities and references can't be encrypted. A custom field can be encrypted when its type istext,emailorurl.- A data export copies the stored ciphertext. Reveal the values in server code before you hand an export to a person.
- The
inlinecustom fields layout keeps values on an adopted table and is not encrypted. - Row policies you add to an app table yourself don't apply to
reveal: it checks the table's own scope, the same one its generated policies use. - The service role and
postgrescan always decrypt. Encryption at rest protects backups, replicas, logs and a leakedselect, not a compromised service key.
ADR 0087 records why keys live per owner in Vault and why decryption runs in definer functions instead of in TypeScript.
Last updated on
Signing keys
Asymmetric keys per owner in Supabase Vault, with rotation that keeps old keys published until their tokens expire, a JWKS endpoint and a signer for JWTs and Ed25519 webhooks.
Shared signals
An OpenID Shared Signals transmitter and receiver, with CAEP and RISC events as signed Security Event Tokens, pushed through outgoing webhooks or polled.