Your instinct is right. Rules for bring-your-own-key:
- The key goes in once and never comes back out to the browser. The frontend sends it to a server function on save; after that the UI only shows
sk-...a1b2. - Store it encrypted with a secret the database doesn't hold. In Supabase, Vault does exactly this (encrypted at rest, key managed outside your tables). Or encrypt in your Edge Function with a key from an env var (AES-GCM via Web Crypto) and store only the ciphertext.
- Calls to OpenAI happen server-side: the browser calls your Edge Function, which checks the user, decrypts their key, calls OpenAI, returns the result.
- RLS: nobody, not even the owner, can
selectthe encrypted column from the client. Only the function (service role) reads it.
That way a leaked table dump or a buggy policy gives an attacker ciphertext, not working keys.