# Production cutover — api.drape.to → ECS/RDS (2026-08-31)

This is the operational record of executing the [ADR-0002](../decisions/0002-lightsail-to-ecs-rds-migration.md) prod cutover. `api.drape.to` now serves from the ECS Fargate prod stack over a valid ACM certificate. The Lightsail box was already gone before this work; the trigger was **Kenji reporting the landing-page sign-up (waitlist) was erroring** — root cause was that `api.drape.to` no longer resolved.

:::note[Result]
`https://api.drape.to` is live: `/api/status` → `200 {"status":"ok","database":"postgresql"}` with a valid cert, `/api/health` → healthy (redis ok, postgresql ok, fal + fashn configured), and the LP waitlist `POST /api/marketing/waitlist` returns success from `https://drape.to`.
:::

## What changed

```mermaid
flowchart LR
    LP["drape.to (Amplify LP)"] -->|POST /api/marketing/waitlist| API["api.drape.to"]
    API --> R53["Route 53 alias"]
    R53 --> ALB["drape-alb (443, ACM cert)"]
    ALB -->|host api.drape.to| PTG["drape-prod-tg"]
    PTG --> SVC["ECS drape-prod x1"]
    SVC --> DB[("RDS drape-postgres-prod-live<br/>encrypted, private")]
```

| Piece | Before | After |
| --- | --- | --- |
| `api.drape.to` DNS | no record (Lightsail gone) | Route 53 **alias A** → `drape-alb` |
| TLS | old ACM cert timed out; legacy cert was `CN=staging.drape.to` (iOS-ATS blocker) | new **ACM cert** for `api.drape.to` + `dev-api.drape.to`, issued and attached to a 443 listener |
| ALB listener | HTTP:80 only | added **HTTPS:443** (TLS13-1-2), host rule `api.drape.to` → `drape-prod-tg` |
| `drape-prod` service | `desired=0` (off) | `desired=1`, healthy on task def `drape-prod:2` |
| prod DB | `drape-postgres-prod` (never turned on, contents unverified) | `drape-postgres-prod-live` — restored from the live data |
| prod task def secrets | 3 keys injected (`DATABASE_URL`, `JWT_SECRET_KEY`, `ADMIN_PASSWORD`) | **14** keys injected (adds FAL, FASHN, Supabase, Stripe, Upstash, OpenAI, Sentry) |

## Procedure executed

1. **ACM.** Old cert had `VALIDATION_TIMED_OUT`. Requested a fresh DNS-validated cert (`api.drape.to` + `dev-api.drape.to`); added the two validation CNAMEs in Route 53; cert **ISSUED**.
2. **Safety.** Manual pre-cutover snapshots of **both** RDS instances (`drape-postgres` and `drape-postgres-prod`).
3. **Live-data restore.** Encrypt-copied the `drape-postgres` snapshot (source was unencrypted; kept prod encrypted via the existing KMS key) and restored it to a **new** instance `drape-postgres-prod-live` — private subnets (`drape-pg-subnets`), same SG, not public.
4. **Repoint.** Updated `prod-keys` `DATABASE_URL` to the restored instance (reusing the copied DB's working credentials), preserving all other fields.
5. **Secrets.** Pulled the real SaaS keys from the old **Render** `Drape-backend` service into `prod-keys` (values never surfaced — names-only tooling). Registered task def **`drape-prod:2`** injecting every non-placeholder key, then scaled `drape-prod` to 1.
6. **Ingress.** Created the HTTPS:443 listener with the new cert + the `api.drape.to` → `drape-prod-tg` rule; added the Route 53 alias.
7. **Verify.** DNS resolves to the ALB; cert verifies; `/api/status` + `/api/health` healthy; waitlist write succeeds over HTTPS with `Origin: https://drape.to`.

:::tip[Reversible by design]
The other session's original `drape-postgres-prod` was left **untouched** as a fallback, and pre-cutover snapshots of both databases are retained. Rollback = point the `api.drape.to` alias/host-rule back, or restore from a snapshot.
:::

## Reference (us-east-1, acct 230790682930)

- ALB `drape-alb` — `drape-alb-1305867817.us-east-1.elb.amazonaws.com` (zone `Z35SXDOTRQ7X7K`)
- Prod service `drape-prod` / target group `drape-prod-tg` / task def `drape-prod:2` at cutover (inventory 2026-09-17 saw `drape-prod:10` / `drape-dev:22` on cluster **`drape`**)
- Prod DB `drape-postgres-prod-live` (encrypted, private) — cutover pointed `drape/backend/prod-keys` `DATABASE_URL` here. **Stage 2A did not re-read secret values**; treat the host mapping as **docs-supported**, not API-proven from the secret payload.
- Cert `arn:aws:acm:us-east-1:230790682930:certificate/19f9816e-8896-4a4b-9a60-c03c133888ba`
- Route 53 zone `drape.to` — `Z00780563EFBECXPWSBK4`

## Open items

:::warning[Follow-ups]
- **Catalog appears empty.** Prod and dev both return empty `/api/products`, `/api/brands`, `/api/store/`, `/api/stats/active-users`. Prod is a faithful copy of dev, so nothing was lost in the cutover — but confirm whether the historical catalog/users are actually in RDS or were left on the (now-gone) Lightsail box. Needs a direct row-count check.
- **9 keys still placeholder** in `prod-keys`: `RESEND_*`, `MUX_*`, `STRIPE_PRICE_*` (4), `INBOUND_EMAIL_WEBHOOK_SECRET`, plus Langfuse (blank in Render too). Email, video, and subscription-pricing features stay off until filled; then add them to the task def.
- **Revoke the temporary Render API key** (`temp-key-recovery-revoke-after`) used to recover the secrets.
- **RDS `drape-postgres-prod` is a LEGACY CANDIDATE / rollback leftover — not approved for deletion.** Stage 2A classification: `drape-postgres-prod-live` = ACTIVE PROD (docs-supported); `drape-postgres` = probable ACTIVE DEV (public; README shortcut; not secret-read proven). Do not decommission any of the three instances from this record.
:::

:::note[Stage 2A — 2026-09-17]
Live inventory confirmed cluster name **`drape`**, ALB host-header routing, and
all three RDS instances still present. See the
[AWS system map](../architecture/aws-system-map.md).
:::
