# Backend deploys (Render Mongo + ECS Postgres)

Drape ships **two** FastAPI backends from `Drape-AI-LLC/Drape-`:

| Code | Host | Trigger |
|---|---|---|
| `backend/` (Mongo monolith) | **Render** | GitHub Actions deploy hooks (this page) |
| `backend-postgres/` | **ECS Fargate** cluster `drape` | Dev: push to `develop` (`backend-postgres/**`); prod: `workflow_dispatch` only (`confirm=deploy-prod`) |

The Postgres path is **not** Render. Live AWS resources (ALB host rules, ECR
`drape-backend-postgres`, RDS classifications) are on
[Infrastructure](../architecture/infrastructure.md) and the
[AWS system map](../architecture/aws-system-map.md). The 2026-08-31 cutover
record is [Production cutover](./prod-cutover.md).

The rest of this page is the **Mongo / Render** pipeline. The rationale for
gating that path is [ADR-0001](../decisions/0001-gated-ci-cd-deploy-pipeline.md).
Postgres env split (dev vs prod, no staging) is
[ADR-0002](../decisions/0002-lightsail-to-ecs-rds-migration.md).

This is a different pipeline from the web tier ([Web deploys](./web-deploys.md),
AWS Amplify) and the mobile app (EAS).

## Postgres on ECS (summary)

| Env | Service | Trigger | Workflow / gate |
|---|---|---|---|
| Dev | `drape-dev` | push to `develop` with path filter `backend-postgres/**` | `.github/workflows/deploy-dev.yml` — AWS auth via OIDC role `drape-github-deploy-role` |
| Prod | `drape-prod` | `workflow_dispatch` **only** | required confirmation input `confirm=deploy-prod` |

- Images: ECR `drape-backend-postgres` (tags `prod` / `dev`).
- Cluster name **`drape`** (not `drape-prod`). Services `drape-prod` and
  `drape-dev`.
- `api.drape.to` → ALB host rule → prod TG. `dev-api.drape.to` and the ALB
  default action → dev TG.
- Secrets inject from Secrets Manager (`drape/backend/prod-keys`,
  `drape/backend/shared-keys`) — **names only**; `DATABASE_URL` host mapping is
  docs-supported, not secret-read proven. RDS instance classes stay on the
  [AWS system map](../architecture/aws-system-map.md):
  `drape-postgres-prod-live` ACTIVE PROD (docs-supported);
  `drape-postgres` **probable** ACTIVE DEV (not API-verified);
  `drape-postgres-prod` LEGACY CANDIDATE, **not approved for deletion**.

## Mongo on Render — the two GitHub Actions workflows

```mermaid
graph LR
    subgraph GH["GitHub · Drape-AI-LLC/Drape-"]
      S[push to staging] --> WS[deploy-staging.yml]
      M[manual run on main] --> WP[deploy-production.yml]
    end
    WS -->|checks pass| HS[staging deploy hook]
    WP -->|manual + checks pass| HP[prod deploy hook]
    HS --> RS[(Render: drape-backend-staging)]
    HP --> RP[(Render: drape-backend)]
```

| Environment | Trigger | Gate | Workflow |
| --- | --- | --- | --- |
| Staging | push to `staging` (`backend/**`) | checks pass (auto) | `deploy-staging.yml` |
| Production | **manual** — Actions → Run workflow on `main` | the manual trigger + checks | `deploy-production.yml` |

Production is manual because GitHub Environment **required-reviewer** protection needs
GitHub Team/Enterprise for a private repo, which this org isn't on. The manual trigger
is the human gate. See [ADR-0001](../decisions/0001-gated-ci-cd-deploy-pipeline.md) for
the rationale and the upgrade path.

Both workflows: install deps → syntax check → critical-import check → `validate_env.py`
(warn-only in CI) → fire the Render **deploy hook** → poll `/api/health` until the
live `commit` equals the pushed short SHA (so a green result means the *new* code is
live, not the old instance).

## One-time setup

**Repo side — already done** (Aug 2026): the `PROD_BACKEND_URL` variable is set, and
production is a manual `workflow_dispatch` workflow (no environment protection needed
on the current plan). Nothing else is required in GitHub for production.

**Render side — the remaining step (Kenji).** Until the deploy-hook secret exists,
the deploy job fails fast with a clear "secret not set" message.

1. Render dashboard → `drape-backend` → Settings → **Deploy Hook** → copy the URL.
2. Repo → Settings → Secrets and variables → Actions → add secret
   **`RENDER_DEPLOY_HOOK_URL`** (paste the hook). *(Or, from a machine with `gh`:
   `gh secret set RENDER_DEPLOY_HOOK_URL --body "<hook-url>"`.)*

**For staging CD (later).** Create a `drape-backend-staging` Render service tracking
the `staging` branch with its own database, then add:

| Type | Name | Value |
| --- | --- | --- |
| Secret | `RENDER_STAGING_DEPLOY_HOOK_URL` | staging deploy hook |
| Variable | `STAGING_BACKEND_URL` | the staging service URL |

## Day-to-day flow

1. Branch from `main`, make the backend change, open a PR into `main`. CI
   (`backend-checks.yml`, `predeploy.yml`) runs on the PR.
2. **Apply Supabase migrations first** if the change needs them — schema leads code.
3. Merge to **`staging`** to exercise it: `deploy-staging.yml` auto-deploys on green.
   Smoke-test against `STAGING_BACKEND_URL`.
4. Merge to **`main`**. Before deploying, confirm Supabase migrations are applied and
   `backend/scripts/predeploy_check.sh` passed locally.
5. Deploy production: **Actions → Deploy Production (manual) → Run workflow → branch
   `main`**. It runs the checks, fires the deploy hook, and verifies the new commit is
   live in `/api/health`. (From CLI: `gh workflow run deploy-production.yml --ref main`.)

## Rollback

Deploys are just the deploy hook pointed at a commit. To roll back:

1. `git revert` the bad commit on `main` (or check out the last good SHA), and push.
2. Approve the resulting `deploy-production.yml` run.
3. The health poll confirms the restored commit is live.

Render's dashboard also offers "Rollback to this deploy" on a prior successful
deploy as a faster path in an incident; follow it with the `git revert` so the repo
and the live service don't diverge.

## Related

- Decision & rationale: [ADR-0001](../decisions/0001-gated-ci-cd-deploy-pipeline.md)
- Local pre-push gate: `backend/scripts/predeploy_check.sh`, `backend/scripts/validate_env.py`
- Health endpoints: `/api/health` (liveness + `commit`), `/api/health/deep` (read/write checks)
