Skip to content

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

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:

  1. Check Railway dashboard — Is the backend running? Look at deployment status and logs.
  2. Check Vercel dashboard — Is the frontend deployed? Check deployment status.
  3. Check Supabase dashboard — Is the database up? Look at the health indicator.
  4. If a service is down, check the service's status page (Railway, Vercel, Supabase all have status pages).
  5. 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.
  6. 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.