Deployment Sop
Civix API Deployment SOP
Overview
This guide covers deploying the Civix Interview API to Vercel. The API works with or without full authentication - it uses device IDs for rate limiting.
Auth Model: No user sign-in required. The mobile app sends a device ID header, and the API uses that for rate limiting and session management.
Deploy without Upstash: You can deploy with only OPENAI_API_KEY. The API enforces in-memory rate limits and in-memory session fallback by default; Upstash Redis is still recommended for durable multi-instance production protection.
Preview deployments: If you only deploy from main, you can ignore or disable Vercel PR previews in the project settings. Production deploys on push to main.
Security overview
- Auth: Device-based identification (
X-Device-IDorX-User-ID); no user sign-in. Session ownership is enforced: chat, evaluate, and complete routes verifysession.userId === deviceIdand return 403 on mismatch. - Rate limiting: Per identifier (device/user/IP) via Upstash Redis when configured, with automatic in-memory fallback if Redis is not configured/available. Configure Upstash for durable multi-instance production protection.
- Input guardrails: User message and answer length limits (2000 / 1000 chars), type checks, and normalization; errors do not echo raw input.
- CORS: API allows only configured origins (your domains, localhost, Expo,
*.vercel.app). Set env or adjustproxy.tsif you add new front-end origins. - Secrets: Use Vercel env vars for
OPENAI_API_KEYand Upstash; never commit secrets.
Troubleshooting: If the build fails with "No Next.js version detected", set Root Directory to apps/web in Vercel project settings.
Automated tests: Before or after deploy you can run: cd apps/web && npm run test (API route tests) and npm run test:e2e (browser E2E). See docs/TESTING.md.
Prerequisites
You'll need accounts for:
- Vercel (free) - https://vercel.com
- OpenAI (paid, ~$0.002/interview) - https://platform.openai.com
- Upstash (free tier available) - https://upstash.com
Step 1: Get OpenAI API Key
- Go to https://platform.openai.com/api-keys
- Click "Create new secret key"
- Name it "Civix Production"
- Copy the key (starts with
sk-) - Save it somewhere safe - you can't see it again
Cost: ~$0.002 per interview (GPT-4o-mini)
Step 2: Create Upstash Redis Database
- Go to https://console.upstash.com
- Click "Create Database"
- Settings:
- Name:
civix-ratelimit - Region: Choose closest to your users (e.g., US East)
- Type: Regional (free tier)
- Name:
- Click "Create"
- In the database details, copy the REST URL and REST token. In Vercel you will add them as
UPSTASH_REDIS_URL(paste the REST URL value) andUPSTASH_REDIS_TOKEN(paste the REST token value). The app expects these env names, notUPSTASH_REDIS_REST_*.
Free tier: 10,000 commands/day (plenty for rate limiting)
Step 3: Deploy to Vercel
Option A: Via Vercel Dashboard (Easiest)
- Go to https://vercel.com/new
- Import your GitHub repo
- Set Root Directory to
apps/web - Add Environment Variables:
Optional (for rate limiting):OPENAI_API_KEY=sk-your-key-hereUPSTASH_REDIS_URL=https://your-redis.upstash.io UPSTASH_REDIS_TOKEN=your-token-here - Click Deploy
Option B: Via CLI
# Install Vercel CLI
npm i -g vercel
# Navigate to web directory
cd apps/web
# Login to Vercel
vercel login
# Deploy (follow prompts)
vercel
# Set environment variables (required)
vercel env add OPENAI_API_KEY
# Optional: For rate limiting
vercel env add UPSTASH_REDIS_URL
vercel env add UPSTASH_REDIS_TOKEN
# Deploy to production
vercel --prod
Step 4: Verify Deployment
Test the health endpoint:
curl https://your-domain.vercel.app/api/health
Expected response:
{
"status": "ok",
"services": {
"openai": "configured",
"upstash": "configured"
}
}
Verify status page: Open https://your-domain.vercel.app/status (or https://status.civixapp.us if configured). You should see "Operational" when OpenAI is configured; "Not operational" otherwise. No secrets are shown.
Step 5: Test Interview API
The interview flow is now phase-locked and timed:
- Conversational phases (
welcome,oath,identity,n400_review) usePOST /api/interview/chat. - Test phases (
reading_test,writing_test,civics_test) usePOST /api/interview/evaluatewith a matchingtype. - Civics evaluation is server-ordered and uses the active question/token pair from API responses.
# Start an interview
curl -X POST https://your-domain.vercel.app/api/interview/start \
-H "Content-Type: application/json" \
-H "X-Device-ID: test-device-123" \
-d '{"mode": "standard", "language": "en"}'
Expected response:
{
"sessionId": "session_xxx",
"phase": "welcome",
"message": "Good morning! Please, have a seat...",
"phaseDeadlineAt": 1739512345678,
"nextPromptType": "question"
}
Advance the session with chat until a test phase:
curl -X POST https://your-domain.vercel.app/api/interview/chat \
-H "Content-Type: application/json" \
-H "X-Device-ID: test-device-123" \
-d '{"sessionId":"session_xxx","userMessage":"Yes, I am ready."}'
When phase is civics_test, evaluate only the active server question:
curl -X POST https://your-domain.vercel.app/api/interview/evaluate \
-H "Content-Type: application/json" \
-H "X-Device-ID: test-device-123" \
-d '{"sessionId":"session_xxx","type":"civics","questionId":42,"answerToken":"token_from_activeCivicsQuestion","userAnswer":"The Constitution"}'
Expected behavior:
200for in-order evaluations with matching phase/type.409+OUT_OF_ORDERfor mismatched phase/type, stale civics question IDs, or invalid civics tokens.- Responses include timing fields (
phaseDeadlineAt,remainingSec) and next civics question metadata when applicable.
Optional full smoke script (recommended):
cd apps/web
npm run test:interview-api
This validates GET /api/health plus ordered start -> chat (conversation) -> evaluate (test phases) -> complete in one run.
If INTERVIEW_SECURITY_MODE=enforce, run smoke with the shared signing secret:
cd apps/web
INTERVIEW_SIGNING_SECRET=your-secret npm run test:interview-api
Environment Variables Reference
| Variable | Required | Description |
|---|---|---|
OPENAI_API_KEY | Yes* | OpenAI API key for GPT-4o-mini |
OPENAI_DAILY_REQUEST_CAP | No | Hard daily cap for OpenAI requests (server-side guardrail) |
OPENAI_BUDGET_MONTHLY_USD | No | Estimated monthly OpenAI budget cap in USD |
OPENAI_ESTIMATED_COST_PER_REQUEST_USD | No | Cost estimate used for budget math (default: 0.002) |
OPENAI_ALERT_THRESHOLD_PCT | No | Comma-separated warning thresholds (default: 80,90,100) |
UPSTASH_REDIS_URL | No | Redis URL for rate limiting |
UPSTASH_REDIS_TOKEN | No | Redis token |
SESSION_STORE_STRICT | No | true to fail closed when Redis session store is unavailable (default: false) |
INTERVIEW_SECURITY_MODE | No | Signed-request mode: off (default), report, or enforce |
INTERVIEW_SIGNING_SECRET | No** | HMAC secret for X-Request-Signature validation |
*Without OpenAI key, API uses fallback scripted responses (good for testing)
**Required when INTERVIEW_SECURITY_MODE=enforce
Note: No JWT/auth secrets needed! The API uses device IDs for identification.
API Authentication Model
No user accounts required! The API uses device IDs:
Mobile App API
│ │
│── X-Device-ID: abc123 ────────▶│
│ │ Rate limit by device ID
│◀─────── Response ──────────────│
The mobile app sends:
X-Device-IDheader: Unique device identifier (from Expo)
That's it! No JWT tokens, no login, no passwords.
Signed request hardening (optional, recommended for production)
Interview endpoints can validate signed requests using:
X-Request-Signature-VersionX-Request-TimestampX-Request-NonceX-Request-Signature
Signature contract:
v2canonical payload:METHOD + PATH + TIMESTAMP + NONCE + DEVICE_ID + SHA256(BODY)enforcemode requiresv2reportmode acceptsv1andv2for phased rollout visibility
Rollout modes:
off: no signature checksreport: checks and logs suspicious payloads without blockingenforce: blocks unsigned, replayed, stale, or invalidly signed requests
Important:
- Public mobile apps cannot safely hold
INTERVIEW_SIGNING_SECRET. - Keep mobile traffic on
offorreportunless requests are routed through a trusted server-side signer/proxy or a stronger auth layer.
Interview Mode Policy (Server-side)
Server mode behavior is centralized in apps/web/lib/interview/mode-policy.ts:
| Mode | Civics Asked | Civics Pass | English Reading/Writing |
|---|---|---|---|
standard | 20 | 12 | Required |
65_20 | 10 | 6 | Waived |
55_15 | 10 | 6 | Waived |
/api/interview/chat, /api/interview/evaluate, and /api/interview/complete all use the same mode policy to prevent rule drift.
Rate Limits
| Endpoint | Limit | Window |
|---|---|---|
/api/interview/start | 10 sessions | 24 hours |
/api/interview/chat | 100 requests | 1 hour |
/api/interview/evaluate | 50 requests | 1 hour |
| All endpoints | 1000 requests | 24 hours |
Limits are per device ID. Premium users can have higher limits (check RevenueCat in mobile app).
Connecting Mobile App
The mobile app is already wired via apps/mobile/lib/api-client.ts. It uses EXPO_PUBLIC_API_URL (e.g. https://api.civixapp.us). For production builds set that env in EAS or your build profile. The client sends X-Device-ID and implements startInterview, sendMessage, evaluateAnswer, completeInterview, and isAPIAvailable. Unit tests: cd apps/mobile && npm test -- --testPathPattern=api-client. See docs/TESTING.md.
Troubleshooting
"OpenAI not configured" in health check
- Verify
OPENAI_API_KEYis set in Vercel - Redeploy after adding env vars
OPENAI_BUDGET_EXCEEDED / budget cap reached
- Verify
OPENAI_DAILY_REQUEST_CAPandOPENAI_BUDGET_MONTHLY_USDvalues - Check fallback behavior in chat/evaluate flow (should continue with non-AI logic)
- Increase caps only after reviewing usage and conversion metrics
"Upstash not configured" warning
- API still works and enforces in-memory fallback limits
- For production durability across instances, add Upstash credentials
CORS errors from mobile app
- Expo development URLs are allowed by default
- Add production domain to
middleware.tsif needed
Rate limit exceeded (429)
- Wait for reset time (shown in response)
- Or increase limits in
lib/ratelimit.ts
Cost Estimation
| Users/Day | Interviews/Day | OpenAI Cost | Upstash |
|---|---|---|---|
| 100 | 100 | ~$0.20/day | Free |
| 1,000 | 1,000 | ~$2/day | Free |
| 10,000 | 10,000 | ~$20/day | ~$5/mo |
Vercel free tier: Plenty for most apps
Quick Deploy Checklist
Required:
- OpenAI account created (platform.openai.com)
- OpenAI API key generated
- Vercel project created (vercel.com)
- Root directory set to
apps/web -
OPENAI_API_KEYenvironment variable added - Deployed to production
- Health endpoint verified (
/api/health) - Test interview started successfully
Optional (recommended for production):
- Upstash account created (upstash.com)
- Upstash Redis database created
-
UPSTASH_REDIS_URLadded -
UPSTASH_REDIS_TOKENadded
See also: For the full go-live list (domains, status.civixapp.us, mobile API URL, smoke test), see Ultimate launch checklist in apps/web/README.md.
Last updated: 2026-02-14