# Deploying to AWS Amplify Hosting
First-time walkthrough for shipping this app on AWS Amplify Hosting. Amplify
auto-detects Next.js, runs the build, and routes SSR pages + API routes
through managed Lambda behind a CloudFront CDN. The `amplify.yml` and
`.nvmrc` in this repo are the only pieces Amplify needs from the codebase.
## Before you start
- An AWS account with billing enabled. Amplify Hosting has a free tier
(1,000 build minutes + 15 GB served + 100 GB-hours of SSR Lambda per
month for the first year) — typical traffic for this app stays comfortably
inside it.
- GitHub access to `joelbrock/WebForm-mw`. Amplify connects via OAuth or a
GitHub App; either works.
- The three required CiviCRM env vars in hand: `CIVI_BASE_URL`,
`CIVI_API_KEY`, `CIVI_SITE_KEY`. Plus the optional `CIVI_HTTP_AUTH_*`
pair if CiviCRM sits behind webserver-level Basic Auth.
## Step 1 — Create the Amplify app
1. Open the Amplify console:
- Pick a region near you / near CiviCRM. `us-east-1` (N. Virginia) is
the default and has the lowest cold-start latency for most setups.
2. Click **Create new app** → **Host web app**.
3. Choose **GitHub** as the source. Authorize Amplify if prompted.
The first time, AWS will install a GitHub App on your account; grant
it access just to `joelbrock/WebForm-mw` (not "all repos").
4. Pick the repo `joelbrock/WebForm-mw` and the `main` branch.
5. **App name:** something like `coop-checkin` (this becomes the
`*.amplifyapp.com` subdomain).
6. Amplify reads `amplify.yml` automatically; you do not need to edit
the build spec on this screen. Click **Next**.
## Step 2 — Environment variables and Secrets
Amplify Gen 2 splits configuration across **two separate pages** in the
console sidebar (under **Hosting**):
- **Environment variables** — plaintext-at-rest, intended for non-sensitive
config. Readable by anyone with `amplify:GetApp` permission on the AWS
account.
- **Secrets** — values stored in AWS Systems Manager Parameter Store as
SecureString, encrypted at rest. Injected into the build/runtime as
normal environment variables under the same name, so application code
doesn't need to know the difference.
Add each variable to the page indicated below:
| Variable | Page | Notes |
|---|---|---|
| `CIVI_BASE_URL` | Environment variables | e.g. `https://crm.fci.coop` |
| `CIVI_API_KEY` | **Secrets** | Required |
| `CIVI_SITE_KEY` | **Secrets** | Required |
| `CIVI_HTTP_AUTH_USER` | Environment variables | Only if CiviCRM has webserver-level Basic Auth in front of it |
| `CIVI_HTTP_AUTH_PASS` | **Secrets** | Only if above set |
| `HEALTH_TOKEN` | **Secrets** | Optional; gates `/api/health` in production |
| `PREVIEW_ADMIN_TOKEN` | **Secrets** | Optional; gates `/api/preview-link` |
Rule of thumb: if leaking the value would let someone impersonate the app
to CiviCRM, or bypass a gate, it goes in **Secrets**. Hostnames and
usernames are fine in plaintext Environment variables.
> **Don't set `NODE_ENV=production` in either page.** Amplify and Next
> already set it correctly at runtime; setting it at build time causes
> `npm ci` to skip devDependencies, which breaks the Tailwind/PostCSS
> step. The `amplify.yml` in this repo guards against this with
> `--include=dev`, but it's cleaner not to set it at all.
> **Don't duplicate names across both pages.** If the same variable
> name appears in both Environment variables and Secrets, Amplify's
> precedence is undefined in Gen 2.
Click through, review, **Save and deploy**.
## Step 3 — First build
The first build takes about 4–6 minutes (later builds are 2–3 with cache).
You can watch progress in the **Hosting environments** view. When it
completes, three things are confirmed:
- The build succeeded → `next build` produced `.next/`.
- SSR functions deployed → API routes and dynamic pages have a Lambda
function behind them.
- The app is reachable at `https://main..amplifyapp.com`.
Smoke-test the deploy:
```
https://main..amplifyapp.com/healthz
```
Should return `200 OK` (lightweight platform check, doesn't hit CiviCRM).
If you set `HEALTH_TOKEN`, the richer diagnostic at
`/api/health?token=` will probe the CiviCRM connection itself —
useful to confirm env vars are wired correctly.
## Step 4 — Custom domain (optional)
Amplify can attach a custom domain (e.g. `survey.fci.coop`) with
auto-issued TLS in a few minutes:
1. In the app, **Hosting** → **Custom domains** → **Add domain**.
2. Enter the apex (`fci.coop`) or a subdomain.
3. Amplify suggests DNS records (CNAME / ALIAS). Add them at your DNS
provider (Cloudflare, Route53, Namecheap, etc.).
4. Wait 5–15 minutes for validation and certificate issuance.
The `*.amplifyapp.com` URL keeps working alongside the custom domain.
## Step 5 — Per-PR previews (optional, recommended)
Amplify can build a preview environment for every pull request:
- **Hosting** → **Previews** → enable for the `joelbrock/WebForm-mw` repo.
- PRs get their own `https://pr-..amplifyapp.com` URL,
posted as a comment on the PR.
- Previews inherit the main branch's environment variables unless you
override on the preview branch.
Useful for reviewing form changes against live CiviCRM DEV before merging.
## What it'll cost
Realistic estimate for this app's expected traffic (a few hundred
form/report loads per month, very modest API throughput):
- **Hosting (CDN + static)**: pennies. Free tier covers it.
- **SSR Lambda**: ~$0.05–$0.50/month depending on traffic. Each form load
triggers a couple of API calls into CiviCRM via Lambda; each call is a
short-lived invocation.
- **Build minutes**: free tier covers up to ~300 small builds/month.
Expect **<$5/month** total at launch traffic. If usage grows
significantly, the costs scale roughly linearly with Lambda invocations.
## Things to know going in
- **Cold starts.** First request after a quiet period adds 500–1500ms of
Lambda init. Subsequent requests reuse the warm container. For an
internal-use form this is fine; users see the loading state during
init.
- **Outbound IPs are not static.** Amplify SSR Lambda functions egress
through AWS-managed IPs that change. If CiviCRM has an IP allowlist
on its API, this won't work out of the box — you'd need a VPC + NAT
Gateway + Elastic IP setup (significantly more complex). Most
CiviCRM auth uses API keys, not IP allowlisting, so this usually
isn't an issue.
- **No Render-style background workers.** Amplify is request/response
only. Anything cron-shaped needs EventBridge + a separate Lambda.
The current app doesn't have background jobs, so this doesn't apply.
- **Logs live in CloudWatch.** Each SSR function has its own log group;
click into the function from the Amplify app view to jump to logs.
Retention defaults to "never expire" — switch to 30 days unless you
need more.
- **Secrets rotate manually.** No auto-rotation. To rotate
`CIVI_API_KEY`, update it in Amplify env vars → trigger a redeploy.
- **`render.yaml` is now informational.** It's still in the repo for the
Render path; Amplify ignores it. If you commit fully to Amplify and
abandon Render, the `render.yaml` and the Render section of
`DEPLOYMENT.md` can be removed.
## When something breaks
- **Build fails on `npm ci`** → usually a Node version mismatch. The
`.nvmrc` pins Node 20; if Amplify's build image doesn't have it, the
`amplify.yml` `nvm install` falls back. Open the build log, search
for "Node version".
- **Build fails on `next build`** → typecheck or lint error. Reproduce
locally with `npm run build`.
- **Site builds but routes 503** → SSR Lambda misconfig. Check the
function's CloudWatch logs for the actual error. Most common cause:
missing required env var (the app refuses to boot in production stub
mode and the `lib/env.ts` validator throws).
- **CiviCRM calls fail with 401** → API key or Basic Auth credentials
wrong. Re-check `CIVI_API_KEY` and `CIVI_HTTP_AUTH_*` in the Amplify
env-vars panel; redeploy after changes.
## After the first successful deploy
- Bookmark the Amplify app URL and the CloudWatch log group.
- Run the email/link smoke test from `EMAIL_DELIVERY.md` against the
Amplify URL to confirm `/api/preview-link` produces working tokenized
URLs end-to-end.
- Update any docs / staff runbooks pointing at the old Render URL.