···77- Never run the project directly (pnpm `dev` or `start`). I'll run any build or serve operations manually through `make` targets in a separate terminal.
88- Use `pnpm exec` instead of `npx` to run local binaries (e.g. `pnpm exec vitest run ...`).
99- When listing a `make` target always specify all variables that need to be set like the domain or the AWS profile.
1010-- By default, the AWS profile names for local development and AWS are `localstack` and `production`, respectively.
1010+- By default, the AWS profile names for local development and AWS are `localstack` and `production`, respectively. A third profile, `staging` (IAM Identity Center — start sessions with `aws sso login --profile staging`), targets the staging AWS account.
1111- `DOMAIN_NAME` is not needed for local `make` targets (only for production targets).
1212+- `ENV=staging` on any make target selects the staging environment in one variable (implies `AWS_PROFILE=staging`, `PROJECT_NAME=atproto-blog-staging`, `DOMAIN_NAME=staging.lemma.pub`). See `STAGING.md` for setup, bring-up, and teardown.
1213- Never suggest actions that cause CloudFormation stack drift (e.g. manually terminating EC2 instances, deleting resources outside of CloudFormation). Always go through CloudFormation to make infrastructure changes.
13141415## Important reference material
+2
PROD.md
···11# Production
2233+For the disposable staging environment in the second AWS account (`staging.lemma.pub`), see `STAGING.md`.
44+35## Additional Production Prerequisites
4657- A registered domain name with Route 53 as the DNS service.
+108
STAGING.md
···11+# Staging
22+33+A disposable staging environment in a **second AWS account** at `staging.lemma.pub`, managed with AWS Organizations. The main account stays the payer (consolidated billing) and keeps the `lemma.pub` hosted zone; the staging account owns only a delegated `staging.lemma.pub` zone and its own copy of every stack.
44+55+All commands below use placeholders like `<staging-account-id>` — never commit real account IDs or email addresses to this file.
66+77+## One-time: organization and account
88+99+1. Create the organization (skip if one exists). The account you run this from becomes the management (payer) account:
1010+ ```sh
1111+ aws organizations create-organization --feature-set ALL --profile production
1212+ ```
1313+2. Create the staging member account (the email must be unique across all AWS accounts; a `+` alias works):
1414+ ```sh
1515+ aws organizations create-account --email <you+aws-staging@example.com> --account-name lemma-staging --profile production
1616+ aws organizations list-create-account-status --states IN_PROGRESS --profile production # poll until SUCCEEDED
1717+ ```
1818+ Consolidated billing is automatic — staging usage appears as a line item on the main account's bill.
1919+3. Enable **IAM Identity Center** (console, management account, one time): create your user, an `AdministratorAccess` permission set, and assign both to the staging account. Then configure the CLI profile:
2020+ ```sh
2121+ aws configure sso # session name e.g. "lemma", start URL from Identity Center, account lemma-staging, profile name "staging"
2222+ ```
2323+ Sessions are short-lived; start each work session with:
2424+ ```sh
2525+ aws sso login --profile staging
2626+ ```
2727+4. Recommended: create an AWS Budget in the management account scoped to the staging linked account (e.g. alert at $20/month) as a safety net for forgotten teardowns.
2828+2929+## One-time: DNS delegation
3030+3131+1. Create the staging hosted zone **in the staging account**. This zone is created outside CloudFormation on purpose: `teardown-aws` never touches it, so the delegation below survives every rebuild cycle (a hosted zone costs $0.50/month).
3232+ ```sh
3333+ aws route53 create-hosted-zone --name staging.lemma.pub --caller-reference "staging-$(date +%s)" --profile staging
3434+ ```
3535+ Note the four name servers in `DelegationSet.NameServers`.
3636+2. Delegate from the main account by adding one NS record set to the `lemma.pub` zone:
3737+ ```sh
3838+ aws route53 change-resource-record-sets --profile production \
3939+ --hosted-zone-id <lemma-pub-zone-id> \
4040+ --change-batch '{
4141+ "Changes": [{
4242+ "Action": "CREATE",
4343+ "ResourceRecordSet": {
4444+ "Name": "staging.lemma.pub",
4545+ "Type": "NS",
4646+ "TTL": 300,
4747+ "ResourceRecords": [
4848+ {"Value": "<ns-1>"}, {"Value": "<ns-2>"}, {"Value": "<ns-3>"}, {"Value": "<ns-4>"}
4949+ ]
5050+ }
5151+ }]
5252+ }'
5353+ ```
5454+3. Verify: `dig NS staging.lemma.pub` returns the staging zone's name servers.
5555+5656+## Bring-up
5757+5858+`ENV=staging` on any make target selects the staging account, project name, and domain in one variable (`AWS_PROFILE=staging`, `PROJECT_NAME=atproto-blog-staging`, `DOMAIN_NAME=staging.lemma.pub`). The distinct `PROJECT_NAME` matters: S3 bucket names are globally unique and production owns the `atproto-blog*` names.
5959+6060+1. Base stacks + code (the `aws` target runs `deploy-aws.sh`, which reads its configuration from the environment):
6161+ ```sh
6262+ aws sso login --profile staging
6363+ export IFRAMELY_API_KEY=<staging-or-shared-key> # mind shell history (HISTCONTROL)
6464+ AWS_PROFILE=staging PROJECT_NAME=atproto-blog-staging API_DID=did:web:staging.lemma.pub make -C infra aws
6565+ ```
6666+2. Domain, TLS, CDN, DNS records, and the SPA:
6767+ ```sh
6868+ make -C infra setup-aws-domain ENV=staging
6969+ ```
7070+ This issues the ACM cert (DNS-validated against the delegated zone), deploys WAF + CloudFront, creates the `staging.lemma.pub` and `api.staging.lemma.pub` records, redeploys the API with the right `AllowedOrigin`, and deploys the site. `did.json` and `oauth-client-metadata.json` are generated for `staging.lemma.pub` into the build output — the committed files in `public/` keep production values.
7171+3. Everyday update targets take `ENV=staging` too, e.g.:
7272+ ```sh
7373+ make -C infra update-aws-api ENV=staging
7474+ make -C infra deploy-site ENV=staging
7575+ make -C infra create-aws-api ENV=staging
7676+ ```
7777+7878+### Stripe (test mode)
7979+8080+Staging uses Stripe **test mode** with its own webhook endpoint — production keys never enter the staging account:
8181+8282+1. Stripe Dashboard (test mode): create the Pro product/price, and a webhook endpoint `https://api.staging.lemma.pub/webhooks/stripe` with events `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`. Save a default Customer Portal configuration in test mode.
8383+2. ```sh
8484+ make -C infra create-aws-secret ENV=staging STRIPE_PRICE_ID=<test price_...>
8585+ aws secretsmanager put-secret-value --profile staging --secret-id /blog/stripe-secret-key --secret-string <sk_test_...>
8686+ aws secretsmanager put-secret-value --profile staging --secret-id /blog/stripe-webhook-secret --secret-string <whsec_...>
8787+ make -C infra create-aws-api ENV=staging
8888+ make -C infra update-aws-api ENV=staging
8989+ ```
9090+ Test cards: `4242 4242 4242 4242`. Leaving the Stripe secrets/price unset simply disables billing (custom domains ungated) — fine for tests that don't touch billing.
9191+9292+## Teardown
9393+9494+```sh
9595+make -C infra teardown-aws ENV=staging CONFIRM=atproto-blog-staging
9696+```
9797+9898+Deletes every stack in reverse dependency order, deleting CloudFront distribution tenants and emptying S3 buckets first, and force-deletes the SecretsManager secrets so an immediate rebuild doesn't collide with names scheduled for deletion. Guard rails: refuses `AWS_PROFILE=production` or `DOMAIN_NAME=lemma.pub`, and requires `CONFIRM` to match the project name exactly.
9999+100100+**Kept on purpose:** the `staging.lemma.pub` hosted zone (and the NS delegation in the main account). Rebuilding is just the two Bring-up steps again.
101101+102102+## Notes
103103+104104+- A full teardown/bring-up cycle is roughly half an hour each way — CloudFront distribution creation/deletion dominates; ACM issuance is a few minutes once DNS resolves.
105105+- The staging indexer consumes the same public Jetstream firehose as production, so staging indexes real public publications into its own DynamoDB table. Expected and harmless.
106106+- Branding strings (ToS/FAQ copy, footer links) still say `lemma.pub` on staging — cosmetic only.
107107+- The WAF WebACL (including its geo-block rules) is deployed to staging unchanged.
108108+- OAuth sign-in on staging works because `did:web:staging.lemma.pub` resolves via the generated `did.json` and the PDS fetches `https://staging.lemma.pub/oauth-client-metadata.json` live.