BYOK Pattern (Bring Your Own Key)
Pattern for supporting both a free tier and bring-your-own-key in public AI experiments. BYOK users provide their own API key and use their own quota. Use-at-your-own-risk.
How It Works
Three tiers of AI access:
- Anonymous / Free -- Google Gemini Flash, rate-limited (10 req/day per IP)
- Authenticated -- Same free provider, higher limits (20 req/day)
- BYOK -- User provides their own API key, uses their own quota
Key Storage
BYOK keys stored in localStorage (client-side only). Keys are sent per-request in the request body to the API route, which proxies to the AI provider. Keys never touch the database.
Why localStorage:
- Keys stay on the user's device
- No server-side storage liability
- Survives page refreshes
- User can clear anytime
- Acceptable for a use-at-your-own-risk experiment
Why NOT cookies/database:
- Cookies send keys on every request (unnecessary exposure)
- Storing user API keys in your database creates liability
- This is an experiment, not a key management service
Client-Side Implementation
// lib/api-keys.ts
const STORAGE_KEY = 'ai-experiment-keys'
type StoredKeys = {
provider: 'openai' | 'anthropic' | 'google'
apiKey: string
}
export function getStoredKey(): StoredKeys | null {
if (typeof window === 'undefined') return null
const raw = localStorage.getItem(STORAGE_KEY)
return raw ? JSON.parse(raw) : null
}
export function setStoredKey(provider: string, apiKey: string) {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ provider, apiKey }))
}
export function clearStoredKey() {
localStorage.removeItem(STORAGE_KEY)
}Settings UI
Simple form in a settings page or modal:
// components/api-key-input.tsx
'use client'
import { useState } from 'react'
import { getStoredKey, setStoredKey, clearStoredKey } from '@/lib/api-keys'
export function ApiKeySettings() {
const stored = getStoredKey()
const [provider, setProvider] = useState(stored?.provider ?? 'openai')
const [key, setKey] = useState(stored?.apiKey ?? '')
return (
<div>
<p>Bring your own API key for unlimited usage.</p>
<p>Keys are stored in your browser only. Use at your own risk.</p>
<select value={provider} onChange={e => setProvider(e.target.value)}>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="google">Google Gemini</option>
</select>
<input
type="password"
value={key}
onChange={e => setKey(e.target.value)}
placeholder="sk-... or your API key"
/>
<button onClick={() => setStoredKey(provider, key)}>Save</button>
<button onClick={() => { clearStoredKey(); setKey('') }}>Clear</button>
{stored && <p>Using: {stored.provider} key</p>}
</div>
)
}Server-Side: Proxying BYOK Keys
The API route receives the key in the request body, creates a provider instance with it, and proxies the request. The key is never logged or stored server-side.
// app/api/chat/route.ts
import { streamText } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
import { createAnthropic } from '@ai-sdk/anthropic'
import { createGoogleGenerativeAI } from '@ai-sdk/google'
function resolveModel(byok?: { provider: string; apiKey: string }) {
if (byok?.apiKey) {
switch (byok.provider) {
case 'openai':
return createOpenAI({ apiKey: byok.apiKey })('gpt-4o')
case 'anthropic':
return createAnthropic({ apiKey: byok.apiKey })('claude-sonnet-4-5-20250929')
case 'google':
return createGoogleGenerativeAI({ apiKey: byok.apiKey })('gemini-2.5-flash')
}
}
// Default: free tier (app's Gemini key)
return createGoogleGenerativeAI({
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY!
})('gemini-2.5-flash')
}
export async function POST(req: Request) {
const { messages, byok } = await req.json()
const model = resolveModel(byok)
const result = await streamText({ model, messages })
return result.toDataStreamResponse()
}Client-Side: Sending Keys Per-Request
'use client'
import { useChat } from '@ai-sdk/react'
import { getStoredKey } from '@/lib/api-keys'
export function Chat() {
const stored = getStoredKey()
const { messages, input, handleInputChange, handleSubmit } = useChat({
body: {
byok: stored ?? undefined,
},
})
// ... render chat UI
}Rate Limiting with BYOK
BYOK users bypass the app's rate limits (they're paying with their own key). Only rate-limit free tier users.
const { byok } = await req.json()
if (!byok?.apiKey) {
const { success } = await rateLimit.limit(ip)
if (!success) return Response.json({ error: 'Rate limit exceeded' }, { status: 429 })
}Security Considerations
What's safe:
- Keys in localStorage are same-origin only (no cross-site access)
- Keys sent over HTTPS to your API route
- API route proxies to provider and discards key
- No server-side logging of keys
What's NOT safe (and that's ok for an experiment):
- Browser extensions can read localStorage
- XSS vulnerability would expose keys
- No key validation before storage
- No encryption at rest in the browser
Mitigations to consider later:
- Validate key format before saving (starts with
sk-, etc.) - Test key with a lightweight API call before saving
- Show a clear disclaimer about risks
UI States
No key stored:
[Free Tier] Using Google Gemini (10 requests/day remaining)
[Add your own key for unlimited usage]
Key stored:
[BYOK] Using OpenAI (your key) -- unlimited
[Change key] [Remove key]
Rate limited (free tier):
[Rate limit reached] Try again tomorrow, or add your own API key