Skip to content

Production Launch Checklist

Info

Status (2026-07-01): The dev environment is fully live on AWS — all CDK stacks deployed, CI/CD green, HTTPS working, DB + Redis wired, admin reachable. Prod is not deployed (config is staged in .aws/iac/config/prod/ + buildspec-prod.yaml).

Items are split into phases. Phase 1 covers everything needed to go live. Later phases cover handoff / hardening and can follow. Boxes checked below are done at the architecture/dev level; prod-specific execution is unchecked. The full bring-up runbook is deployment.md.

Phase 1: Getting to Production

Infrastructure Setup

The prod bring-up mirrors the dev sequence, which is fully proven end-to-end.

  • IaC authored (AWS CDK) — shared / network / compute / storage, validated on dev
  • CI/CD pipeline built and proven (GitHub Actions → OIDC → CodeBuild → ECR → ECS)
  • CDK bootstrapped + shared (ECR/OIDC) + CodeConnections GitHub credential — account-wide, reused by prod
  • Client production domain confirmed (currently a placeholder cslewis.fueled.engineering; set SITE_DOMAIN in buildspec-prod.yaml + DJANGO_ALLOWED_HOSTS)
  • Deploy prod stacks: network → compute → storage (multi-AZ Aurora min 1 / max 8 ACU + reader; ASG 2–4 × t3.small; Redis cache.t4g.small)
  • Push seed image, then first real prod deploy via a v*.*.* tag
  • Scheduled jobs (payments reconciliation, CSL-412) — the daily reconcile_entitlements EventBridge schedule is defined in compute-stack.ts for dev and prod, but activates only on the next cdk deploy cslewis-compute-<env> (the app tag/merge deploy does not run cdk), alongside the pending SES task-role grant. After deploying, confirm the schedule exists and that a manual run-task of the command succeeds.
  • Request prod ACM cert, DNS-validate (validation CNAME DNS-only), set acm_ssl_cert in config/prod/compute.yaml, redeploy compute (adds :443 + 80→443 redirect)
  • Cloudflare (prod): proxied DNS → prod ALB; SSL Full (strict) via a per-hostname Configuration Rule (not the zone default); ALB SG already locked to Cloudflare IPs
  • GitHub prod environment protection rules (required reviewers before a prod deploy)
  • Transactional email (Amazon SES — CSL-401) — shared sender for auth magic links + DMCC reminders, on mail.cslewis.com. SES is deployed + verified in prod; the remaining items are the launch gates:
    • Email stack deployed (cslewis-email-prod, 2026-09-16). The task-role SES grant on cslewis-compute-prod is written but not yet deployed — it rides the next compute deploy (only the DMCC send path needs it, and that is not live).
    • Cloudflare (cslewis.com zone): DKIM CNAMEs, MAIL FROM MX + SPF TXT, and _dmarc TXT (p=none) published; SES shows DKIM + MAIL FROM verified. Tighten _dmarc to p=quarantine after a clean week.
    • SES production access granted (out of the sandbox, 50k/day).
    • Firebase Console: custom SMTP → SES (email-smtp.us-east-2.amazonaws.com:587, START_TLS) configured with the derived password; confirmed CUSTOM_SMTP active. Sender not yet aligned with the /finishSignIn action-link domain (CSL-65) — not required for inbox delivery.
    • Subscribe a monitored address to the bounce/complaint SNS topic (set alert_email in config/prod/email.yaml, redeploy the email stack, confirm the SNS subscription email). Until then the reputation alarms notify nobody.
    • Verify end-to-end: mail-tester.com + a fresh Gmail receives the magic link in the inbox, not spam, with a tappable link (closes CSL-155). Currently blocked on a Firebase daily email quota, not SES.

Application Secrets

Populate the cslewis-app-prod Secrets Manager JSON (created empty by CDK). Generate prod-only values — do not reuse dev.

  • DATABASE_URL — from the prod Aurora writer endpoint + rds-credentials-prod
  • REDIS_URL — prod ElastiCache endpoint
  • DJANGO_SECRET_KEY — freshly generated, prod-only
  • SENTRY_DSN — prod Sentry project/environment (also still pending on dev)
  • AI tagging — via AWS Bedrock (task-role bedrock:InvokeModel, no API key); us-east-2 needs a cross-region inference profile. ANTHROPIC_API_KEY dropped (decided 2026-07-06)
  • ELEVENLABS_API_KEY — audio pre-generation
  • REVENUECAT_API_KEY — RevenueCat v2 secret key for the customer current-state sync (CSL-355). Seeded in cslewis-app-prod (separate key from dev). Raise the scope to Customer information → Read & Write (dev and prod): account deletion deletes the RevenueCat customer (CSL-478), and a Read key gets 403, which only queues the delete for the daily job to retry forever.
  • REVENUECAT_PROJECT_ID — the RevenueCat project id the v2 endpoints are scoped to (proj7d44db36); the sync fails without it. Seeded.
  • REVENUECAT_WEBHOOK_TOKEN — the Authorization value set on the RevenueCat webhook; the webhook fails closed (401) with no value configured. Seeded; Production webhook configured (Environment = Production).
  • REVENUECAT_WEBHOOK_HMAC_SECRET — HMAC signing enabled on the Production webhook; 64-hex secret seeded. Webhook loop verified live on prod (v0.1.11, 2026-09-17): valid signed → 200, bad/missing signature → 401, wrong token → 401.
  • API_KEYS — the /api/ app-gate key (ADR-0005); generate a prod-only value and share it with the mobile client. Required before prod serves /api/ — the gate fails closed (403) with no key configured. (Dev value already set.)
  • FIREBASE_SERVICE_ACCOUNT_KEY — once Firebase auth lands (ADR-0003); add the key to the task-def secrets map + render-task-defs.sh whitelist
  • Email (SES) needs no app secret — the backend sends via the ECS task role (SES v2 API, CSL-401). The only static SES credential is the Firebase SMTP password, which lives in Firebase's SMTP config, not this JSON (see the Amazon SES item under Infrastructure Setup).

Initial Data Population (in order)

  • Migration flow proven (one-off ECS task runs migrate before each rollout)
  • Prod migrations run (automatic on first prod deploy)
  • Create prod superuser (one-off createsuperuser --noinput task; rotate the password afterward)
  • Configure the Wagtail Site record (hostname, root page) for the prod domain
  • Content migration — the biggest risk. No raw content exists yet; the importer + AI-tag drafting + editor QA loop must run and be reviewed before launch (see the content-migration risk assessment). This is the long pole, not infra.

Feature flags at launch

  • Enable the journey gate (CSL-356 AC-1). buildspec-prod.yaml sets JOURNEY_GATE_ENABLED: "False" so the app stays usable before launch; buildspec-dev.yaml already sets it "True" for paywall testing. At launch, set prod to "True" and deploy on a vX.Y.Z tag (the prod workflow triggers on v*.*.*). Flipping it on does not reset readers: the free-journey grant is still claimed on first Start while the gate is off, so returning readers keep the journey they began with. After deploy, confirm in-container that JOURNEY_GATE_ENABLED resolves to True.

Pre-launch Polish (non-blocking)

  • Rotate the temporary dev superuser password (was passed via a task override → recorded in CloudTrail)
  • Provision Celery worker + beat services (not yet in the stack) for Daily Drop scheduling, AI tagging, ElevenLabs, and Substack sync
  • Real legal copy (privacy policy, terms) — required for App Store review
  • Account-deletion endpoint built + production-tested (App Store mandate) once auth lands
  • Google Play account-deletion URL (CSL-385) — the public page /delete-account/ is served by interfaces/web on every env (no app, no login). Register it in Play Console → App content → Data safety → Data deletion, and confirm the SUPPORT_EMAIL address is correct.
  • Self-service deletion on the web (CSL-478) — /delete-account/ signs the reader in with a Firebase email link and calls DELETE /api/account/. Per Firebase project (dev cs-lewis-fueled, prod cs-lewis-app):
    • Auth → Sign-in method: Email link (passwordless) enabled.
    • Auth → Settings → Authorized domains: the backend domain (dev-cslewis.fueled.engineering, app.cslewis.com).
    • Auth → Templates: the email action URL stays the default https://<project>.firebaseapp.com/__/auth/action. A custom action URL on /finishSignIn would send the web link into the app.
    • On a phone with the app installed: request a link from /delete-account/, open it, and confirm it finishes in the browser, not the app (iOS and Android).
    • Update the Privacy Policy retention statement in the CMS (Legal and Support → Privacy Policy) to match the page: subscription event history under an anonymous ID, deletion log lines for 14 days, the anonymous ID until a failed subscription-provider delete is retried successfully, encrypted backups for 7 days. Legal owns the wording.
  • Universal Links / App Links (CSL-55) — association files are served by interfaces/web on every env (/.well-known/apple-app-site-association, /.well-known/assetlinks.json, /finishSignIn). To go live in prod:
    • Add the prod Android signing fingerprints to ANDROID_SHA256_FINGERPRINTS in buildspec-prod.yaml (CSL-466). Both are configured now: the shared upload key mobile signs every build with, and the Play App Signing key Play installs carry. Before tagging, verify both against mobile's actual current signing certificates (Play Console → App integrity, and the upload keystore) — a stale or wrong fingerprint here fails App Links silently, not loudly.
    • Add the prod universal-link domain to Firebase Auth Authorized domains; set the mobile ActionCodeSettings.url to https://<prod-domain>/finishSignIn.
    • Verify no-redirect + application/json: curl -sSI https://<prod-domain>/.well-known/apple-app-site-association (and .../assetlinks.json); confirm via Apple's CDN + Google Digital Asset Links.
  • Decide + resolve the mobile API framework — DRF + Wagtail (ADR-0004)
  • Build the read API (DRF function-based views + drf-spectacular OpenAPI)

Capacity / Tuning (post-load-test)

  • Run a load test against the dive-deeper and today's-passage endpoints
  • Validate Aurora ACU floor/ceiling (1–8), ASG (2–4), and Redis sizing against results
  • Confirm p95 latency targets; right-size t3.small if the web tier is tight
  • Confirm the CI rollout has spare capacity on prod (2+ hosts → minHealthyPercent 100 keeps at least the desired task count healthy during rollouts; dev fits both tasks on a single t3.medium)

Phase 2: Hardening / Edge (optional, can defer)

Some of this is already in the baseline IaC/app; the rest is prod hardening.

  • Origin lockdown — ALB security group restricted to Cloudflare's published IP ranges (ports 80 + 443)
  • TLS + HSTS — Cloudflare Full (strict) + ALB 80→443 redirect; Django sets HSTS (1y, preload), secure cookies, SECURE_SSL_REDIRECT, nosniff
  • Deploy safety — ECS circuit breaker + auto-rollback; images retained in ECR; data stores RemovalPolicy.RETAIN (+ prod Aurora deletion protection)
  • Authenticated Origin Pulls (mTLS) between Cloudflare and the prod ALB (stronger than IP allow-listing alone)
  • Cloudflare WAF + rate limiting tuned (free-tier baseline today); rate-limit write endpoints (django-ratelimit) once the API exists
  • CDN for the API — deferred per ADR-0001 (Redis at origin is sufficient at MVP scale); revisit when telemetry warrants edge caching. CloudFront already fronts S3 media.
  • Dependency/CVE scanning in CI; review ECR scan-on-push findings
  • No PR CI gate exists yet — add lint/test checks required before merge to main

Phase 3: Repo / Ownership Handoff

Do these together with stakeholders after launch is stable.

  • Repo / ownership transfer to the client (if in scope)
  • Tighten the OIDC trust policy from repo:Fueled/cs-lewis-backend:* to specific environments/branches
  • Branch protection on main + required checks; tighten deploy triggers
  • Cost review against the ~$350/mo estimate; consider a reserved/savings plan
  • Backup/restore drill (Aurora PITR, S3 media object-version) and an on-call/runbook handoff — procedures in Backup and Restore. Prod media versioning is enabled and the DB keeps 7-day PITR; this item is the drill and ownership handoff.
  • Revisit dev-only conveniences (single instance on t3.medium) so they don't leak into prod expectations