Firebase-Main Holding URL

The firebase-main branch and nest-firebase.thephenom.app form a frozen snapshot of the currently-deployed N.E.S.T. production. They exist so that forward migration work on main always has a working fallback to point to.

TL;DR. nest-firebase.thephenom.app serves whatever was on phenom-backend:main at commit b46e3d1 (the “Firebase-era” cohort label — the codebase had already removed direct Firebase calls in 45689f2, but the deploy continued to be referred to as the Firebase-era snapshot). The branch is frozen at the application-code layer and auto-deploys on push.

Why this exists

Phenom is mid-migration off the Firebase-era N.E.S.T. architecture. Every forward step on main (auth changes, Worker rewrites, dashboard polish) is one step further from the version that’s currently working in production. Without a parallel deploy, if main ever ships something broken we have nothing to fall back to other than a git revert and a redeploy.

firebase-main is the fallback. nest-firebase.thephenom.app is the URL pointing at it.

Topology

graph LR
    Branch(["github.com/Phenom-earth/phenom-backend<br/>branch: firebase-main"])
    Pages(["CF Pages<br/>phenom-backend-firebase"])
    Worker(["CF Worker<br/>nest-api-firebase"])
    Domain(["nest-firebase.thephenom.app"])
    Access(["CF Access<br/>nest-firebase app"])
    Cognito(["AWS Cognito<br/>shared with prod"])
    Hasura(["Hasura GraphQL<br/>chat-testing.thephenom.app"])

    Branch -->|"push triggers GH Actions"| Pages
    Branch -->|"push triggers GH Actions"| Worker
    Domain -->|"DNS proxied CNAME"| Pages
    Domain -->|"routes /api/* + /health"| Worker
    Access -->|"gates"| Domain
    Worker -->|"Bearer JWT"| Hasura
    Worker -.->|"verifies JWT"| Cognito
SurfaceResourceSame-as-prod?
Branchfirebase-main on Phenom-earth/phenom-backendApplication code yes; .github/workflows/nest-ci.yml has one extra commit adding the firebase-main deploy jobs.
Pages projectphenom-backend-firebase (production_branch=firebase-main)Independent project; isolated deploy history.
Pages subdomainphenom-backend-firebase.pages.devIndependent.
Worker scriptnest-api-firebaseBuilt from the same admin_sandbox/nest-api/ source as prod’s nest-api. Isolated script (wrangler deploy --name nest-api-firebase).
Worker routesnest-firebase.thephenom.app/api/*, nest-firebase.thephenom.app/healthnest-api-firebaseIsolated.
Custom domainnest-firebase.thephenom.appProxied CNAME → phenom-backend-firebase.pages.dev.
CF Access appnest-firebase — gated by the GitHub-IdP INT policy. (nest, nest-prod, dev-nest CF Access apps were all deleted on 2026-05-16; only nest-firebase keeps the gate now because the snapshot is intentionally team-only.)
Cognitous-east-1_AkG9mnbjA user pool, client 2eq1vf0nvl5o3rha2vshm8j0mnSame as prod.
Hasurachat-testing.thephenom.app/v1/graphqlSame as prod (per layer-4 workflow env).

How deploys work

Pushing to firebase-main triggers two jobs in .github/workflows/nest-ci.yml:

  • Layer 5a — Deploy nest-api-firebase Worker — runs npx wrangler deploy --name nest-api-firebase from admin_sandbox/nest-api/. The --name override is the load-bearing piece: it deploys the same code as nest-api but as a distinct Worker script, so future Worker work on main cannot accidentally overwrite the holding URL’s Worker.
  • Layer 5b — Deploy to nest-firebase.thephenom.app — builds admin_sandbox/phenom/ with the same env vars as Layer 4b (VITE_HASURA_GRAPHQL_URL=https://chat-testing.thephenom.app/v1/graphql, VITE_COGNITO_CLIENT_ID=2eq1vf0nvl5o3rha2vshm8j0mn, etc.) and runs npx wrangler pages deploy dist --project-name=phenom-backend-firebase --branch=firebase-main.

Both jobs are gated on github.ref == 'refs/heads/firebase-main' — they don’t fire on any other branch.

Posting commits to firebase-main

Default policy: don’t. firebase-main is a snapshot lane. The application-code tree under admin_sandbox/ should remain byte-identical to whatever was on main at b46e3d1.

The narrow exceptions:

  1. Critical hotfix that has to ship to the holding URL — cherry-pick from main, prefer a CI-only fix to a code change.
  2. CI workflow changes specifically for the firebase-main deploy jobs — those have to live on this branch by GitHub’s “workflow file at the branch HEAD” rule.

Anything else: open the change against main (or its feature branch), let it land there, and decide separately whether the holding URL should track that change. The whole point of the snapshot is that it does NOT automatically inherit new application code.

S3 bucket + IAM principal

nest-api-firebase signs presigned GetObject URLs against the dedicated phenom-firebase-media S3 bucket. The bucket is a frozen mirror of phenom-dev-media-staging as of 2026-05-14; no further writes happen against it. The Worker reads the bucket name from env.S3_BUCKET (set in wrangler.toml [env.firebase.vars]).

Both the bucket and the IAM principal that signs the URLs are managed in phenom-infra/environments/development/nest-api-media.tf:

Terraform resourceAWS name
aws_s3_bucket.firebase_mediaphenom-firebase-media
aws_iam_user.nest_api_firebase_s3phenom-nest-api-firebase-s3
aws_iam_user_policy.nest_api_firebase_s3 (inline)GetMediaObjectss3:GetObject + s3:ListBucket scoped to phenom-firebase-media only

Access keys are intentionally not in Terraform state — they live in the Cloudflare Worker secrets AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY on the nest-api-firebase script and are rotated via wrangler secret put. This mirrors the App Runner credential pattern already established in phenom-infra.

Worker secrets

The nest-api-firebase Worker is deployed with the same code shape as nest-api but without secrets at first deploy — Cloudflare doesn’t let secrets be cloned because they’re write-only at the API surface. The four secrets it needs:

cd ~/c/phenom-backend/admin_sandbox/nest-api
bunx wrangler secret put AWS_ACCESS_KEY_ID            --name nest-api-firebase
bunx wrangler secret put AWS_SECRET_ACCESS_KEY        --name nest-api-firebase
bunx wrangler secret put CF_ACCESS_DOCS_CLIENT_ID     --name nest-api-firebase
bunx wrangler secret put CF_ACCESS_DOCS_CLIENT_SECRET --name nest-api-firebase

For the AWS pair, source the access key created from aws_iam_user.nest_api_firebase_s3 (the IAM user managed in phenom-infra above). For the CF Access pair, source the service token named nest-firebase docs proxy in the Pavestar Cloudflare Access dashboard (already linked to the int-docs Access policy).

Confirm post-set:

bunx wrangler secret list --name nest-api-firebase

CF Access

The nest-firebase Access app shares the same allow-list as the prod nest app via reusable Cloudflare Access policy UIDs (INT and NEST & DEV NEST Service access). Changes to who can reach prod NEST automatically apply to the holding URL — no second policy to maintain.

If you intentionally want the holding URL accessible to a wider/narrower audience than prod, create a new app-specific policy on the nest-firebase app instead of editing the shared INT policy.

Decommissioning

When the holding URL is no longer needed (post-migration confidence restored), tear down in this order to avoid orphan resources:

  1. Disable the CI jobs by removing the firebase-main triggers in .github/workflows/nest-ci.yml on firebase-main (or simply delete the branch).
  2. Delete the Worker routes for nest-firebase.thephenom.app/api/* and /health on the thephenom.app zone.
  3. Delete the Worker script nest-api-firebase.
  4. Detach the custom domain nest-firebase.thephenom.app from the phenom-backend-firebase Pages project.
  5. Delete the DNS CNAME record for nest-firebase in the thephenom.app zone.
  6. Delete the Pages project phenom-backend-firebase.
  7. Delete the CF Access app nest-firebase.
  8. Delete the branch firebase-main on Phenom-earth/phenom-backend (last — keep the git history available longest).

All eight steps are reversible from CF dashboard if you change your mind mid-teardown.

  • Infrastructure & Service Map — the equivalent map for current production.
  • Admin Dashboard — N.E.S.T. UI documentation.
  • ~/.claude/PAI/MEMORY/WORK/20260513-nest-firebase-holding-branch/ISA.md — the ISA that drove the original stand-up, including 37 verification criteria and their evidence.