My app lets users paste their own OpenAI key so they pay their own usage. Bolt stored it in a api_key text column in the profiles table and the frontend reads it and calls OpenAI directly from the browser. It works but it feels very wrong. If my database leaks, I leak everybody's keys.
What's the normal way to do this?
What I’ve tried
Asked Bolt to 'encrypt the key'. It base64-encoded it, which I know is not encryption. Looked at Supabase Vault but didn't understand if it's meant for this.
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 select the 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.
Adding to Chidi: validate the key when it's saved (one cheap call, e.g. list models) so you store only working keys, and let users delete it themselves. Put in your privacy text that you store it encrypted and what you use it for. People are rightly careful with this.
Nice UX touch: in the 'paste your key' screen, tell users to create a separate key just for your app, with a project budget limit in their OpenAI dashboard. If anything goes wrong they revoke one key and their other projects keep working.