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.
Links¶
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; setSITE_DOMAINinbuildspec-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; Rediscache.t4g.small) - Push seed image, then first real prod deploy via a
v*.*.*tag - Scheduled jobs (payments reconciliation, CSL-412) — the daily
reconcile_entitlementsEventBridge schedule is defined incompute-stack.tsfor dev and prod, but activates only on the nextcdk deploy cslewis-compute-<env>(the app tag/merge deploy does not runcdk), alongside the pending SES task-role grant. After deploying, confirm the schedule exists and that a manualrun-taskof the command succeeds. - Request prod ACM cert, DNS-validate (validation CNAME DNS-only), set
acm_ssl_certinconfig/prod/compute.yaml, redeploycompute(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
prodenvironment 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 oncslewis-compute-prodis 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.comzone): DKIM CNAMEs, MAIL FROM MX + SPF TXT, and_dmarcTXT (p=none) published; SES shows DKIM + MAIL FROM verified. Tighten_dmarctop=quarantineafter 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; confirmedCUSTOM_SMTPactive. Sender not yet aligned with the/finishSignInaction-link domain (CSL-65) — not required for inbox delivery. - Subscribe a monitored address to the bounce/complaint SNS topic (set
alert_emailinconfig/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.
- Email stack deployed (
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.dropped (decided 2026-07-06)ANTHROPIC_API_KEY -
ELEVENLABS_API_KEY— audio pre-generation -
REVENUECAT_API_KEY— RevenueCat v2 secret key for the customer current-state sync (CSL-355). Seeded incslewis-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 gets403, 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— theAuthorizationvalue 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.shwhitelist - 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
migratebefore each rollout) - Prod migrations run (automatic on first prod deploy)
- Create prod superuser (one-off
createsuperuser --noinputtask; rotate the password afterward) - Configure the Wagtail
Siterecord (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.yamlsetsJOURNEY_GATE_ENABLED: "False"so the app stays usable before launch;buildspec-dev.yamlalready sets it"True"for paywall testing. At launch, set prod to"True"and deploy on avX.Y.Ztag (the prod workflow triggers onv*.*.*). 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 thatJOURNEY_GATE_ENABLEDresolves toTrue.
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 byinterfaces/webon every env (no app, no login). Register it in Play Console → App content → Data safety → Data deletion, and confirm theSUPPORT_EMAILaddress is correct. - Self-service deletion on the web (CSL-478) —
/delete-account/signs the reader in with a Firebase email link and callsDELETE /api/account/. Per Firebase project (devcs-lewis-fueled, prodcs-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/finishSignInwould 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/webon 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_FINGERPRINTSinbuildspec-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.urltohttps://<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.
- Add the prod Android signing fingerprints to
-
Decide + resolve the mobile API framework— DRF + Wagtail (ADR-0004) - Build the read API (DRF function-based views +
drf-spectacularOpenAPI)
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.smallif the web tier is tight - Confirm the CI rollout has spare capacity on prod (2+ hosts →
minHealthyPercent 100keeps 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