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-ID or X-User-ID); no user sign-in. Session ownership is enforced: chat, evaluate, and complete routes verify session.userId === deviceId and 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 adjust proxy.ts if you add new front-end origins.
  • Secrets: Use Vercel env vars for OPENAI_API_KEY and 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:

  1. Vercel (free) - https://vercel.com
  2. OpenAI (paid, ~$0.002/interview) - https://platform.openai.com
  3. Upstash (free tier available) - https://upstash.com

Step 1: Get OpenAI API Key

  1. Go to https://platform.openai.com/api-keys
  2. Click "Create new secret key"
  3. Name it "Civix Production"
  4. Copy the key (starts with sk-)
  5. Save it somewhere safe - you can't see it again

Cost: ~$0.002 per interview (GPT-4o-mini)


Step 2: Create Upstash Redis Database

  1. Go to https://console.upstash.com
  2. Click "Create Database"
  3. Settings:
    • Name: civix-ratelimit
    • Region: Choose closest to your users (e.g., US East)
    • Type: Regional (free tier)
  4. Click "Create"
  5. 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) and UPSTASH_REDIS_TOKEN (paste the REST token value). The app expects these env names, not UPSTASH_REDIS_REST_*.

Free tier: 10,000 commands/day (plenty for rate limiting)


Step 3: Deploy to Vercel

Option A: Via Vercel Dashboard (Easiest)

  1. Go to https://vercel.com/new
  2. Import your GitHub repo
  3. Set Root Directory to apps/web
  4. Add Environment Variables:
    OPENAI_API_KEY=sk-your-key-here
    
    Optional (for rate limiting):
    UPSTASH_REDIS_URL=https://your-redis.upstash.io
    UPSTASH_REDIS_TOKEN=your-token-here
    
  5. 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) use POST /api/interview/chat.
  • Test phases (reading_test, writing_test, civics_test) use POST /api/interview/evaluate with a matching type.
  • 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:

  • 200 for in-order evaluations with matching phase/type.
  • 409 + OUT_OF_ORDER for 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

VariableRequiredDescription
OPENAI_API_KEYYes*OpenAI API key for GPT-4o-mini
OPENAI_DAILY_REQUEST_CAPNoHard daily cap for OpenAI requests (server-side guardrail)
OPENAI_BUDGET_MONTHLY_USDNoEstimated monthly OpenAI budget cap in USD
OPENAI_ESTIMATED_COST_PER_REQUEST_USDNoCost estimate used for budget math (default: 0.002)
OPENAI_ALERT_THRESHOLD_PCTNoComma-separated warning thresholds (default: 80,90,100)
UPSTASH_REDIS_URLNoRedis URL for rate limiting
UPSTASH_REDIS_TOKENNoRedis token
SESSION_STORE_STRICTNotrue to fail closed when Redis session store is unavailable (default: false)
INTERVIEW_SECURITY_MODENoSigned-request mode: off (default), report, or enforce
INTERVIEW_SIGNING_SECRETNo**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-ID header: 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-Version
  • X-Request-Timestamp
  • X-Request-Nonce
  • X-Request-Signature

Signature contract:

  • v2 canonical payload: METHOD + PATH + TIMESTAMP + NONCE + DEVICE_ID + SHA256(BODY)
  • enforce mode requires v2
  • report mode accepts v1 and v2 for phased rollout visibility

Rollout modes:

  • off: no signature checks
  • report: checks and logs suspicious payloads without blocking
  • enforce: blocks unsigned, replayed, stale, or invalidly signed requests

Important:

  • Public mobile apps cannot safely hold INTERVIEW_SIGNING_SECRET.
  • Keep mobile traffic on off or report unless 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:

ModeCivics AskedCivics PassEnglish Reading/Writing
standard2012Required
65_20106Waived
55_15106Waived

/api/interview/chat, /api/interview/evaluate, and /api/interview/complete all use the same mode policy to prevent rule drift.


Rate Limits

EndpointLimitWindow
/api/interview/start10 sessions24 hours
/api/interview/chat100 requests1 hour
/api/interview/evaluate50 requests1 hour
All endpoints1000 requests24 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_KEY is set in Vercel
  • Redeploy after adding env vars

OPENAI_BUDGET_EXCEEDED / budget cap reached

  • Verify OPENAI_DAILY_REQUEST_CAP and OPENAI_BUDGET_MONTHLY_USD values
  • 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.ts if needed

Rate limit exceeded (429)

  • Wait for reset time (shown in response)
  • Or increase limits in lib/ratelimit.ts

Cost Estimation

Users/DayInterviews/DayOpenAI CostUpstash
100100~$0.20/dayFree
1,0001,000~$2/dayFree
10,00010,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_KEY environment 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_URL added
  • UPSTASH_REDIS_TOKEN added

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