Provider API keys
Add your own OpenAI, Anthropic, Google, Mistral, Groq, Together, or DeepSeek keys — encrypted in Supabase Vault.
Project88 runs on your provider API keys. You add them once per org; every workspace in the org can use them.
Why your own keys
- Cost — you pay your provider directly, at their rates.
- Quotas — your org's quota is your quota; you're never sharing.
- Security — keys are encrypted in Supabase Vault. The plaintext is
never persisted in
provider_keys.api_key(that column was dropped in migration 014) and never reaches the browser at runtime.
Supported providers
| Provider | Key prefix | Models |
|---|---|---|
| OpenAI | sk-... | gpt-4o, o1, o1-mini, gpt-4o-mini |
| Anthropic | sk-ant-... | Claude 3.5 Sonnet, Claude 3.5 Haiku, Opus |
| (no prefix) | Gemini 2.5 Pro, Gemini 2.5 Flash | |
| Mistral | (no prefix) | Large, Medium, Small |
| Groq | gsk_... | Llama 3.x, Mixtral on Groq |
| Together | (no prefix) | Mixtral, Llama, Qwen, DeepSeek |
| DeepSeek | sk-... | DeepSeek-V3, DeepSeek-R1 |
The UI validates the key prefix client-side before saving so you don't silently store a malformed key.
Add a key
⌘K→ Settings → Providers.- Click + Add key on the provider you want.
- Paste the key. We validate the prefix.
- Give it a key name (free-form — useful when you rotate or have multiple).
- Save.
The key is sent to the insert_provider_key security-definer function,
which:
- Creates a row in
vault.secretswith the plaintext. - Inserts a row in
provider_keyswith the secret ID reference only. - Returns the row to the UI.
The plaintext never returns from the function.
What you can see
The Providers tab shows each key's name, provider, active flag, and created_at. The full key value is never displayed — only fields safe to show.
Dev keys
.env variables (VITE_OPENAI_API_KEY, VITE_ANTHROPIC_API_KEY,
VITE_GOOGLE_API_KEY) can be used in development as a fallback. The
Settings page shows the env-fallback section separately so it's obvious
which keys come from where.
How chat completions use keys
When a chat request comes in:
- The browser calls the
chat-proxyEdge Function with the model name and the conversation. - The proxy looks up the active key for that model's provider via the
get_decrypted_provider_keysecurity-definer function. - The function pulls the plaintext from Vault, returns it to the proxy only (not to the browser).
- The proxy makes the upstream provider call and streams results back to the browser.
The plaintext exists in memory inside the proxy for the lifetime of the request, then is dropped. No logs include the key.
Toggle and delete
Each key has an active toggle (deactivate without deleting) and a
delete button. Deleting calls delete_provider_key, which removes the
Vault secret and the provider_keys row in a single transaction.