Environment & Secrets Inventory
Single source of truth for every API key, env var, and credential used by AEO Bunny.
For deployment procedures, see docs/internal/DEPLOYMENT_RUNBOOK.md.
1. Environment Variable Reference
Core Application
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
ENVIRONMENT |
Railway |
App mode (development / production). Note: Used in code but absent from .env.example. |
Public |
development |
APP_NAME |
Railway |
Application name, used as FastAPI app title. Mapped via pydantic-settings as app_name in app/config.py. Note: Used in code but absent from .env.example. |
Public |
AEO Bunny |
LOG_LEVEL |
Railway |
Logging verbosity (DEBUG / INFO / WARNING) |
Public |
INFO |
DATABASE_URL |
Railway |
PostgreSQL connection string (must use postgresql+asyncpg:// prefix) |
Secret |
SQLite (dev only) |
ALLOWED_ORIGINS |
Railway |
CORS origins, comma-separated (e.g., https://portal.aireadyplumber.com) |
Public |
"" |
ENABLE_GATES |
Railway |
Enable quality gate pause points (true in production) |
Public |
false |
PORTAL_BASE_URL |
Railway |
Frontend URL for generated links (password reset, etc.) |
Public |
http://localhost:3000 |
REDIS_URL |
Railway |
Redis connection string — Upstash (free tier), decided Phase 9b. Note: Defined in config but not yet wired to any application code. Placeholder for future Redis integration. |
Secret |
"" |
PROMPT_DIR |
Railway |
Path to prompt templates |
Internal |
prompts/v1 |
Anthropic (Content Generation)
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
ANTHROPIC_API_KEY |
Railway |
Claude API authentication |
Secret |
"" |
CLAUDE_MODEL |
Railway |
Primary model for writing/analysis |
Internal |
claude-sonnet-4-6 |
CLAUDE_MODEL_FAST |
Railway |
Fast model for dossier extraction, schema gen, alt text, and visibility analysis (with Sonnet escalation) |
Internal |
claude-haiku-4-5-20251001 |
|
|
|
|
|
Supabase (Database + Auth)
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
SUPABASE_URL |
Railway + Vercel |
Supabase project URL |
Public |
"" |
SUPABASE_ANON_KEY |
Railway + Vercel |
Public anonymous key (safe to expose in frontend) |
Public |
"" |
SUPABASE_SERVICE_ROLE_KEY |
Railway only |
Admin key — bypasses RLS, never expose to frontend |
Secret |
"" |
Cloudflare R2 (Storage)
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
R2_ACCOUNT_ID |
Railway |
Cloudflare account identifier |
Internal |
"" |
R2_ACCESS_KEY_ID |
Railway |
R2 API key ID |
Secret |
"" |
R2_SECRET_ACCESS_KEY |
Railway |
R2 API secret |
Secret |
"" |
R2_BUCKET_NAME |
Railway |
Storage bucket name |
Internal |
aeo-bunny-images |
R2_PUBLIC_URL |
Railway |
Public CDN URL for serving images/HTML |
Public |
"" |
AI Visibility (OpenAI + Perplexity)
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
OPENAI_API_KEY |
Railway |
ChatGPT visibility checks (web search model) |
Secret |
"" |
PERPLEXITY_API_KEY |
Railway |
Perplexity Sonar visibility checks |
Secret |
"" |
VISIBILITY_SCORE_TIMEOUT |
Railway |
Seconds per visibility check request |
Internal |
30 |
VISIBILITY_ON_DEMAND_RATE_LIMIT |
Railway |
Rate limit for on-demand scans. Note: Defined in config but not read by application code — rate limit is hardcoded as 3/hour in the visibility endpoint decorator. Changing this env var has no effect (tech debt). |
Internal |
3/hour |
GoHighLevel (Customer Communication)
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
GHL_WEBHOOK_URL |
Railway |
Outbound webhook destination URL (GHL inbound webhook) |
Secret |
"" |
GHL_WEBHOOK_SECRET |
Railway |
Legacy HMAC shared secret for inbound purchase webhook auth. Ed25519 migration is complete — X-GHL-Signature is now primary auth (public key hardcoded in ghl_inbound.py). This variable is retained as legacy fallback only. |
Secret |
"" |
Image Processing
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
MAX_CONCURRENT_IMAGES |
Railway |
Parallel image processing limit |
Internal |
5 |
IMAGE_MAX_WIDTH |
Railway |
Max image width in pixels |
Internal |
1200 |
IMAGE_QUALITY |
Railway |
JPEG/WebP compression quality (0-100) |
Internal |
85 |
UPLOAD_TEMP_DIR |
Railway |
Temporary directory for file uploads |
Internal |
/tmp/aeo_bunny |
Photo Collection (Hardcoded Constants)
The photo gate thresholds are compile-time constants in app/api/photos.py, not env vars. They are not configurable at runtime:
| Constant |
Value |
Meaning |
PHOTO_MINIMUM |
100 |
Minimum photos required to pass GATE_PHOTO_UPLOAD and unblock batch 1 HTML assembly |
PHOTO_TARGET |
150 |
Target photo count shown to customers as the recommended goal |
PHOTO_MAXIMUM |
300 |
Hard cap on photos per project (enforced at upload time) |
To change these values a code deploy is required.
Pipeline Tuning
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
MAX_CONCURRENT_CATEGORIES |
Railway |
Parallel article writing sessions per batch |
Internal |
3 |
MAX_PIPELINE_ATTEMPTS |
Railway |
Max retry attempts for a failed pipeline run |
Internal |
3 |
Revision Control
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
AUTO_APPROVE_REVISIONS |
Railway |
Auto-approve revisions below cost threshold |
Internal |
false |
REVISION_COST_THRESHOLD |
Railway |
Max cost ($) for auto-approval |
Internal |
1.00 |
REVISION_COST_PER_ARTICLE |
Railway |
Estimated cost per article revision |
Internal |
0.06 |
MAX_REVISION_ROUNDS |
Railway |
Max revision cycles per project |
Internal |
3 |
Visibility Intelligence (Alerts)
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
ALERT_SCORE_FLOOR |
Railway |
Minimum score below which drop alerts fire |
Internal |
30.0 |
ALERT_DROP_THRESHOLD |
Railway |
Points drop from peak that triggers an alert |
Internal |
20.0 |
ALERT_COOLDOWN_HOURS |
Railway |
Hours between repeated alerts for same location |
Internal |
168 (7 days) |
TREND_WINDOW_SIZE |
Railway |
Number of recent scores used for trend calculation |
Internal |
10 |
Scheduled Visibility Scans
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
SCAN_ENABLED |
Railway |
Enables or disables the hourly scan scheduler entirely |
Internal |
true |
SCAN_FREQUENCY_EARLY_DAYS |
Railway |
Days between scans during the first 30 days after deployment |
Internal |
7 |
SCAN_FREQUENCY_STEADY_DAYS |
Railway |
Days between scans after the first 30 days |
Internal |
30 |
Visibility Analysis (Deep Analysis Pipeline)
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
ANALYSIS_TIMEOUT_SECONDS |
Railway |
Timeout per Haiku analysis call |
Internal |
30 |
ANALYSIS_MAX_RETRIES |
Railway |
Retry attempts for failed analysis. Note: Defined in config but not read by application code — values are hardcoded as function parameter defaults in sweeper.py. Changing this env var has no effect (tech debt). |
Internal |
3 |
RAW_RESPONSE_MAX_CHARS |
Railway |
Max characters stored per raw AI response. Note: Defined in config but not read by application code — all 4 visibility adapters hardcode 16384. Changing this env var has no effect (tech debt). |
Internal |
16384 |
RAW_RESPONSE_RETENTION_MONTHS |
Railway |
Months before raw responses are eligible for cleanup |
Internal |
6 |
ANALYSIS_SWEEPER_INTERVAL_MINUTES |
Railway |
How often the recovery sweeper runs. Note: Defined in config but not read by application code — values are hardcoded as function parameter defaults in sweeper.py. Changing this env var has no effect (tech debt). |
Internal |
5 |
ANALYSIS_STALE_THRESHOLD_MINUTES |
Railway |
Minutes before a pending analysis is considered stale. Note: Defined in config but not read by application code — values are hardcoded as function parameter defaults in sweeper.py. Changing this env var has no effect (tech debt). |
Internal |
10 |
Readiness Checks
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
PAGESPEED_API_KEY |
Railway |
Google PageSpeed Insights API key (optional, higher rate limits) |
Secret |
"" |
READINESS_HTTP_TIMEOUT |
Railway |
Per-request HTTP timeout (seconds) for crawlability and schema checkers |
Internal |
10 |
READINESS_PAGESPEED_SAMPLE_SIZE |
Railway |
Number of pages sampled per PageSpeed Insights run |
Internal |
5 |
READINESS_SPEED_CHECKER_TIMEOUT |
Railway |
Total wall-clock timeout (seconds) for the entire speed checker (covers all PSI API calls combined) |
Internal |
120 |
Engine Weighting
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
ENGINE_WEIGHTS |
Railway |
JSON config for visibility engine weights. Full 4-engine example: {"chatgpt": 0.35, "perplexity": 0.35, "google_aio": 0.15, "gemini": 0.15}. Weights are renormalized across active (credentialed) engines. Omit a key to use equal distribution. |
Internal |
"" (equal weights across active engines) |
Dormant / Deferred
| Variable |
Where Set |
What It Does |
Sensitivity |
Status |
DATAFORSEO_LOGIN |
Railway |
DataForSEO API login (Google AI Overviews adapter) |
Secret |
Dormant — activate when ready |
DATAFORSEO_PASSWORD |
Railway |
DataForSEO API password |
Secret |
Dormant |
DATAFORSEO_BASE_URL |
Railway |
DataForSEO API endpoint |
Internal |
Dormant — default https://api.dataforseo.com/v3 |
GOOGLE_GEMINI_API_KEY |
Railway |
Google Gemini API key (Gemini visibility adapter) |
Secret |
Dormant — activate when ready |
GOOGLE_GEMINI_MODEL |
Railway |
Gemini model name |
Internal |
gemini-2.0-flash |
Developer / Testing
| Variable |
Where Set |
What It Does |
Sensitivity |
Default |
TEST_DATABASE_URL |
Local only |
PostgreSQL connection string for integration tests (tests/integration/conftest.py). Tests are skipped when absent. Not a production variable — never set in Railway or CI. |
Secret (local) |
"" (tests skipped) |
Frontend (Vercel)
| Variable |
Where Set |
What It Does |
Sensitivity |
NEXT_PUBLIC_API_URL |
Vercel |
Backend API URL (e.g., https://api.aireadyplumber.com) |
Public |
NEXT_PUBLIC_SUPABASE_URL |
Vercel |
Supabase project URL (same as backend) |
Public |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Vercel |
Supabase anon key (same as backend) |
Public |
NEXT_PUBLIC_PORTAL_PREVIEW_MODE |
Vercel |
Set to "1" to enable preview mode (fake data, no backend needed). Used in portal/src/lib/preview-mode.ts. |
Public |
Total: 64 variables (59 backend + 1 developer/testing + 4 frontend)
2. Service Account Inventory
| Service |
Dashboard URL |
Account Owner |
Billing Contact |
Who Has Access |
| Supabase |
supabase.com/dashboard |
[OWNER] |
[OWNER] |
[TEAM_MEMBERS] |
| Railway |
railway.com/project/[ID] |
[OWNER] |
[OWNER] |
[TEAM_MEMBERS] |
| Vercel |
vercel.com/[team]/portal |
[OWNER] |
[OWNER] |
[TEAM_MEMBERS] |
| Cloudflare R2 |
dash.cloudflare.com |
[OWNER] |
[OWNER] |
[TEAM_MEMBERS] |
| Anthropic |
console.anthropic.com |
[OWNER] |
[OWNER's card] |
[OWNER] |
| OpenAI |
platform.openai.com |
[OWNER] |
[OWNER's card] |
[OWNER] |
| Perplexity |
docs.perplexity.ai/account |
[OWNER] |
[OWNER's card] |
[OWNER] |
| GoHighLevel |
app.gohighlevel.com |
[OWNER] |
[OWNER] |
[TEAM_MEMBERS] |
| Google Cloud (PSI) |
console.cloud.google.com |
[OWNER] |
Free tier |
[OWNER] |
| DataForSEO |
app.dataforseo.com |
[OWNER] |
[OWNER's card] |
[OWNER] (Dormant) |
| Google AI Studio / Gemini |
aistudio.google.com |
[OWNER] |
Free tier |
[OWNER] (Dormant — distinct from Google Cloud PSI) |
| Upstash (Redis) |
console.upstash.com |
[OWNER] |
Free tier |
[OWNER] (decided in Phase 9b) |
Action needed: Fill in [OWNER] and [TEAM_MEMBERS] with actual names before go-live.
3. Cost Monitoring
Dashboard Links
| Service |
Usage Dashboard |
What to Watch |
| Anthropic |
console.anthropic.com → Usage |
Token usage per day, cost per customer run |
| OpenAI |
platform.openai.com → Usage |
Web search API calls (visibility checks) |
| Perplexity |
Perplexity account → Billing |
Sonar API calls |
| Railway |
railway.com → Project → Usage |
Compute hours, bandwidth |
| Supabase |
supabase.com → Project → Reports |
Database size, auth requests, egress |
| Cloudflare R2 |
dash.cloudflare.com → R2 |
Storage size, Class A/B operations |
Expected Cost Per Customer
| Component |
Estimated Cost |
Notes |
| Anthropic (content gen) |
$1.50–3.00 |
50 articles × Sonnet, BI/Strategy, Review Agent |
| OpenAI (visibility) |
$0.30–0.50 |
~15 prompts × 2 engines × baseline + post-deploy |
| Perplexity (visibility) |
$0.20–0.50 |
Same prompt count as OpenAI |
| Railway (compute) |
$0.30–0.50 |
Pipeline run time (~20-40 min) |
| R2 (storage) |
$0.01–0.05 |
Images, HTML, ZIPs (minimal) |
| Total per customer |
$2.50–5.50 |
|
Cost understatement warning: These estimates will increase when all 4 visibility engines are active (especially DataForSEO which has per-task pricing). Anthropic costs above also understate actual usage — they do not include visibility analysis Haiku/Sonnet calls, dossier extraction, photo scoring, or revision orchestration.
Cost Alert Threshold
If a single pipeline run exceeds $8.00, investigate:
- Check if the pipeline retried multiple times (failed batches re-running)
- Check if visibility checks ran excessively (rate limit misconfiguration)
- Check Anthropic token usage for the project (unusually long articles or repeated agent calls)
4. Key Rotation Schedule
| Key |
Rotate How Often |
How to Rotate |
ANTHROPIC_API_KEY |
Every 6 months or on suspected compromise |
Generate new key in Anthropic console → update Railway → verify health → revoke old key |
OPENAI_API_KEY |
Every 6 months or on suspected compromise |
Generate new key in OpenAI dashboard → update Railway → verify visibility check → revoke old key |
PERPLEXITY_API_KEY |
Every 6 months or on suspected compromise |
Same pattern as above |
SUPABASE_SERVICE_ROLE_KEY |
Rotate if compromised |
Regenerate in Supabase dashboard → update Railway → redeploy → old key auto-invalidated |
DATABASE_URL (password) |
Every 6 months |
Change password in Supabase → update Railway DATABASE_URL → wait for redeploy → verify health |
R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY |
Every 6 months |
Create new API token in Cloudflare → update Railway → verify upload works → delete old token |
GHL_WEBHOOK_SECRET |
Only if legacy fallback is still in use |
Ed25519 public key is hardcoded in code (not an env var), so GHL key rotation requires a code deploy. GHL_WEBHOOK_SECRET rotation is only relevant for the legacy fallback path — generate new secret → update Railway AND GHL simultaneously. |
GOOGLE_GEMINI_API_KEY |
When activated |
Standard rotation: new key → update → verify → revoke old |
DATAFORSEO_LOGIN/PASSWORD |
When activated |
Standard rotation |
Golden rule: Always update the new key FIRST, verify it works, THEN revoke the old key. Never revoke-then-update.
5. Bus Factor — "If I Get Hit by a Bus"
This section ensures someone else on the team can access everything if the founder is unavailable.
Critical Access Checklist
- [ ] Password manager — All service credentials are stored in [PASSWORD_MANAGER]. Share vault access with [BACKUP_PERSON].
- [ ] Supabase — Invite [BACKUP_PERSON] as org member. They need dashboard access to manage users, run SQL, check auth.
- [ ] Railway — Invite [BACKUP_PERSON] to the project. They need access to view logs, redeploy, update env vars.
- [ ] Vercel — Invite [BACKUP_PERSON] to the team. They need access to redeploy frontend, update env vars.
- [ ] Cloudflare — Invite [BACKUP_PERSON] to the account. They need R2 bucket access.
- [ ] Anthropic — API key is in the password manager. Billing is on [CARD_HOLDER]'s card.
- [ ] OpenAI — API key is in the password manager. Billing is on [CARD_HOLDER]'s card.
- [ ] Perplexity — API key is in the password manager.
- [ ] GoHighLevel — Invite [BACKUP_PERSON] as a user. They need access to automation workflows and webhook configs.
- [ ] GitHub — Invite [BACKUP_PERSON] as a collaborator on the repo. They need push access for emergency deploys.
- [ ] Domain registrar — [BACKUP_PERSON] has account access to manage DNS (critical for domain expiry).
Emergency Playbook
If the founder is unavailable and the system is down:
- Check Railway dashboard — Is the backend running? Look at deployment status and logs.
- Check Vercel dashboard — Is the frontend deployed? Check deployment status.
- Check Supabase dashboard — Is the database up? Look at the health indicator.
- If a service is down, check the service's status page (Railway, Vercel, Supabase all have status pages).
- If an API key expired, find the key in the password manager and regenerate it on the provider's dashboard. Update the env var in Railway/Vercel.
- If you can't fix it, contact [ESCALATION_CONTACT] at [PHONE/EMAIL].
Action needed: Fill in [PASSWORD_MANAGER], [BACKUP_PERSON], [CARD_HOLDER], [ESCALATION_CONTACT] before go-live.