9.2 KiB
9.2 KiB
API Key Management Architecture
Overview
ALwrity supports two deployment modes with different API key management strategies:
- Local Development: API keys stored in
.envfiles for convenience - Production (Vercel + Render): User-specific API keys stored in database with full user isolation
Architecture
🏠 Local Development Mode
Detection:
DEBUG=truein environment variables, ORDEPLOY_ENVis not set
API Key Storage:
- Backend:
backend/.envfile - Frontend:
frontend/.envfile - Database: Also saved for consistency
Flow:
User completes onboarding
↓
API keys saved to database (user-isolated)
↓
API keys ALSO saved to .env files (for convenience)
↓
Backend services read from .env file
↓
Single developer, single set of keys
Advantages:
- ✅ Quick setup for developers
- ✅ No need to configure environment for every user
- ✅ Keys persist across server restarts
🌐 Production Mode (Vercel + Render)
Detection:
DEBUG=falseor not set, ANDDEPLOY_ENVis set (e.g.,DEPLOY_ENV=render)
API Key Storage:
- Backend: PostgreSQL database (user-isolated)
- Frontend:
localStorage(runtime only) - NOT in .env files
Flow:
Alpha Tester A completes onboarding
↓
API keys saved to database with user_id_A
↓
Backend services fetch keys from database when user_id_A makes requests
↓
Multiple users, each with their own keys
↓
Alpha Tester B completes onboarding
↓
API keys saved to database with user_id_B
↓
Backend services fetch keys from database when user_id_B makes requests
Advantages:
- ✅ Complete user isolation - User A's keys never conflict with User B's keys
- ✅ Zero cost for you - Each alpha tester uses their own API keys
- ✅ Secure - Keys stored encrypted in database
- ✅ Scalable - Unlimited alpha testers, each with their own keys
Implementation
1. Backend: User API Key Context
The UserAPIKeyContext class provides user-specific API keys to backend services:
from services.user_api_key_context import user_api_keys
# In your backend service
async def generate_content(user_id: str, prompt: str):
# Get user-specific API keys
with user_api_keys(user_id) as keys:
gemini_key = keys.get('gemini')
exa_key = keys.get('exa')
# Use keys for this specific user
response = await call_gemini_api(gemini_key, prompt)
return response
How it works:
- Development: Reads from
backend/.env - Production: Fetches from database for the specific
user_id
2. Frontend: CopilotKit Key Management
// Frontend automatically handles this:
// 1. Saves to localStorage (for runtime use)
// 2. In dev: Also saves to frontend/.env
// 3. In prod: Only uses localStorage
const copilotApiKey = localStorage.getItem('copilotkit_api_key');
3. Environment Variable Detection
Backend (backend/.env):
# Development
DEBUG=true
# Production
DEBUG=false
DEPLOY_ENV=render # or 'railway', 'heroku', etc.
Render Dashboard:
DEBUG=false
DEPLOY_ENV=render
Vercel Dashboard:
REACT_APP_API_URL=https://alwrity.onrender.com
REACT_APP_BACKEND_URL=https://alwrity.onrender.com
Use Cases
Use Case 1: You (Developer) - Local Development
Setup:
# backend/.env
DEBUG=true
GEMINI_API_KEY=your_personal_key
EXA_API_KEY=your_personal_key
COPILOTKIT_API_KEY=your_personal_key
Behavior:
- You complete onboarding once
- Keys saved to both database AND
.envfiles - All your local testing uses these keys
- No need to re-enter keys
Use Case 2: Alpha Tester A - Production
Setup:
- Alpha Tester A visits
https://alwrity-ai.vercel.app - Goes through onboarding
- Enters their own API keys:
GEMINI_API_KEY=tester_a_gemini_keyEXA_API_KEY=tester_a_exa_keyCOPILOTKIT_API_KEY=tester_a_copilot_key
Behavior:
- Keys saved to database with
user_id=tester_a_clerk_id - When Tester A generates content:
- Backend fetches
tester_a_gemini_keyfrom database - Uses Tester A's Gemini quota
- All costs charged to Tester A's Gemini account
- Backend fetches
Use Case 3: Alpha Tester B - Production (Same Time)
Setup:
- Alpha Tester B visits
https://alwrity-ai.vercel.app - Goes through onboarding
- Enters their own API keys:
GEMINI_API_KEY=tester_b_gemini_keyEXA_API_KEY=tester_b_exa_keyCOPILOTKIT_API_KEY=tester_b_copilot_key
Behavior:
- Keys saved to database with
user_id=tester_b_clerk_id - When Tester B generates content:
- Backend fetches
tester_b_gemini_keyfrom database - Uses Tester B's Gemini quota
- All costs charged to Tester B's Gemini account
- Backend fetches
- Tester A and Tester B completely isolated ✅
Database Schema
-- OnboardingSession: One per user
CREATE TABLE onboarding_sessions (
id SERIAL PRIMARY KEY,
user_id VARCHAR(255) UNIQUE NOT NULL, -- Clerk user ID
current_step INTEGER DEFAULT 1,
progress FLOAT DEFAULT 0.0,
started_at TIMESTAMP DEFAULT NOW(),
completed_at TIMESTAMP
);
-- APIKey: Multiple per user (one per provider)
CREATE TABLE api_keys (
id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES onboarding_sessions(id),
provider VARCHAR(50) NOT NULL, -- 'gemini', 'exa', 'copilotkit'
key TEXT NOT NULL, -- Encrypted in production
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(session_id, provider) -- One key per provider per user
);
Isolation:
- Each user has their own
onboarding_session - Each session has its own set of
api_keys - Query:
SELECT key FROM api_keys WHERE session_id = (SELECT id FROM onboarding_sessions WHERE user_id = ?)
Migration Path
Current State:
- ❌ All users' keys overwrite the same
.envfile - ❌ Last user's keys are used for all users
New State:
- ✅ Development:
.envfile for convenience - ✅ Production: Database per user
- ✅ Complete user isolation
Code Changes Required:
Before (BAD - uses global .env):
import os
def generate_content(prompt: str):
gemini_key = os.getenv('GEMINI_API_KEY') # Same for all users!
response = call_gemini_api(gemini_key, prompt)
return response
After (GOOD - uses user-specific keys):
from services.user_api_key_context import user_api_keys
def generate_content(user_id: str, prompt: str):
with user_api_keys(user_id) as keys:
gemini_key = keys.get('gemini') # User-specific key!
response = call_gemini_api(gemini_key, prompt)
return response
Testing
Test Local Development:
- Set
DEBUG=trueinbackend/.env - Complete onboarding with test keys
- Check
backend/.env- should contain keys ✅ - Generate content - should use keys from
.env✅
Test Production:
- Set
DEBUG=falseandDEPLOY_ENV=renderon Render - User A completes onboarding with keys A
- User B completes onboarding with keys B
- User A generates content - uses keys A ✅
- User B generates content - uses keys B ✅
- Check database:
Should show separate keys for User A and User B ✅
SELECT user_id, provider, key FROM api_keys JOIN onboarding_sessions ON api_keys.session_id = onboarding_sessions.id;
Security Considerations
Production Enhancements (Future):
- Encrypt API keys in database using application secret
- Rate limiting per user to prevent abuse
- Key validation before saving
- Audit logging of API key usage
- Key rotation support
Current Implementation:
- ✅ Keys stored in database (not in code)
- ✅ User isolation via
user_id - ✅ HTTPS encryption in transit
- ⚠️ Keys not encrypted at rest (TODO)
Troubleshooting
Issue: "No API key found"
- Development: Check
backend/.envfile exists and has keys - Production: Check database has keys for this user:
SELECT * FROM api_keys WHERE session_id = (SELECT id FROM onboarding_sessions WHERE user_id = 'user_xxx');
Issue: "Wrong user's keys being used"
- Cause: Service not using
UserAPIKeyContext - Fix: Update service to use
user_api_keys(user_id)context manager
Issue: "Keys not saving to .env in development"
- Cause:
DEBUGnot set totrue - Fix: Set
DEBUG=trueinbackend/.env
Summary
| Feature | Local Development | Production |
|---|---|---|
| Key Storage | .env files + Database |
Database only |
| User Isolation | Not needed (single user) | Full isolation |
| Cost | Your API keys | Each user's API keys |
| Convenience | High (keys persist) | Medium (enter once) |
| Scalability | 1 developer | Unlimited users |
| Detection | DEBUG=true |
DEPLOY_ENV set |
Bottom Line:
- 🏠 Local: Quick setup, your keys,
.envconvenience - 🌐 Production: User isolation, their keys, zero cost for you
This architecture ensures:
- ✅ You can develop locally with convenience
- ✅ Alpha testers use their own keys (no cost to you)
- ✅ Complete user isolation in production
- ✅ Seamless transition between environments