Skip to content

Environments

Staging and prod — the hostnames, how each repo deploys, and why staging is disposable on purpose.

Updated View as Markdown

Two environments: staging and prod. There is deliberately no dev environment — staging doubles as dirty-dev and internal-demo space, which works because tearing it down and standing it back up is routine.

Prod is not stood up yet. Its Terraform inputs are still placeholders, so every prod hostname below is the agreed target rather than a live host.

Hostnames

Surface Staging Prod (target)
Tenant origin — consumer at /, admin at /admin staging.curate.memorial <slug>.curate.memorial
Additional tenants <slug>.staging.curate.memorial <slug>.curate.memorial
Platform super-admin platform-staging.curate.memorial admin.curate.memorial
Backend API api-staging.curate.memorial api.curate.memorial
Marketing + public guides curate.memorial
This handbook internal.curate.memorial

The two content sites are single-environment — one Worker each, no staging copy. They publish content and have nothing to break against a backend.

The staging tenant model

Staging runs one standing internal tenant whose slug is staging, so it lands on staging.curate.memorial — exactly the shape of a prod tenant origin. Staging therefore permanently rehearses tenant onboarding instead of being a special case with its own rules.

A second concurrent tenant — testing cross-tenant isolation, say — gets <slug>.staging.curate.memorial as its own Custom Domain. Plain <slug>.curate.memorial stays reserved for prod tenants; staging must not eat into that namespace. And because staging is on the reserved-slug list, no customer can ever claim it.

Staging is disposable by design

Its Postgres is a single ECS task with no volume and no backups. Teardown and re-spin take about fifteen minutes and cost effectively nothing while the environment is down. That is the property that lets staging be dirty-dev, demo, and error-reproduction space simultaneously.

Three things follow, and they are features rather than bugs:

  • All staging data is gone after a re-spin — tenants, staff, memorials, media. Reseed from the UAT playbook if you need it back.
  • Secrets are re-minted, so the bootstrap super-admin password changes.
  • One manual DNS edit each time: the ALB gets a new DNS name, so the Cloudflare api-staging record has to be repointed.

The frontends are not part of teardown at all. They are Cloudflare Workers; they keep serving throughout, and their /api/* calls simply fail until the backend is back. Nothing to do in either direction.

The full procedure is curate-server docs/DEPLOY-RUNBOOK.md.

How each repo deploys

Repo Mechanism Trigger
curate-server — infrastructure Terraform: make apply ENV=<env> manual, root AWS credentials
curate-server — image GitLab CI → OIDC → ECR → ECS push to main, or a manual web pipeline
curate-tenant Cloudflare Workers Builds push to main
curate-platform Cloudflare Workers Builds push to main
curate-content Cloudflare Workers Builds, one project per site push to main

Terraform owns infra; CI owns the image

The backend’s two paths are decoupled on purpose. The ECS services ignore changes to their task definition, so terraform apply never rolls a service, and a CI deploy never introduces an environment variable — it only swaps the image inside the task definition that already exists.

The consequence to remember: a change needing both a new environment variable and the code that reads it must apply Terraform first. Shipping the image first boots new code against old configuration.

Why the frontends do not deploy from GitLab

Workers Builds keeps the long-lived Cloudflare API token out of GitLab CI variables — wrangler has no OIDC path, unlike the backend’s AWS role. The GitLab pipelines on those repos are merge gates only: typecheck, tests, lint, build.

Non-default branches get preview versions on workers.dev. One caveat worth knowing: a preview hostname’s first label is not a tenant slug, so previews cannot exercise tenant resolution — that only becomes testable on a real Custom Domain.

AWS shape

Everything runs in ap-east-2 (Taipei), one fully isolated stack per environment, named <env>-<project>-*. An ALB fronts two ECS Fargate services — the API and the background worker, from the same image — with S3, KMS, and Secrets Manager alongside. Staging uses Postgres-on-ECS; prod will use RDS. ALB ingress is locked to Cloudflare’s CIDR ranges, so the origin is not reachable except through the edge.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close