Infrastructure & Service Map

Complete map of all services, APIs, databases, and credentials that power the N.E.S.T. (Nexus for Evidence, Screening, and Tracking) platform. Use this when debugging cross-service issues or onboarding new team members.
Audit stamp: Partially Verified — 2026-06-19 — The Narrator
Partially Verified · 2026-06-19 · The Narrator
Source: asset-registry.yaml; nest-ops live at HTTP 200; API at HTTP 500 (degraded); full topology requires AWS console verification
C2PA signed · SanMarcSoft AI content credential

Sensitive Infrastructure: This page documents credential locations and service endpoints. Access is restricted to INT team members via Cloudflare Access.

Service Architecture

graph LR
    Browser(["Browser<br/>N.E.S.T. SPA"])
    CFA(["CF Access<br/>OAuth Gate"])
    Pages(["CF Pages<br/>Static Assets"])
    Worker(["nest-api Worker<br/>/api/* &amp; /health"])
    Cognito(["AWS Cognito<br/>phenom-prod pool"])
    Hasura(["Hasura GraphQL<br/>api.thephenom.app"])
    RDS(["PostgreSQL<br/>AWS RDS"])
    S3(["AWS S3<br/>phenom-prod-media-storage"])
    GitHub(["GitHub App<br/>Org Teams"])
    Synapse(["Synapse<br/>chat.thephenom.app"])

    Browser -->|"HTTPS"| CFA
    CFA -->|"Gate entry"| Pages
    Browser -->|"Cognito SDK"| Cognito
    Cognito -->|"JWT"| Browser
    Browser -->|"Bearer JWT"| Worker
    Worker -->|"Forward JWT"| Hasura
    Hasura -->|"SQL"| RDS
    Worker -->|"Pre-signed URLs"| S3
    Worker -->|"App JWT"| GitHub
    Cognito -.->|"JWT SSO"| Synapse

    style Browser fill:#1a1a2e,stroke:#e94560,color:#fff,rx:12,ry:12
    style CFA fill:#16213e,stroke:#0f3460,color:#fff,rx:12,ry:12
    style Pages fill:#16213e,stroke:#0f3460,color:#fff,rx:12,ry:12
    style Worker fill:#0f3460,stroke:#e94560,color:#fff,rx:12,ry:12
    style Cognito fill:#1a1a2e,stroke:#e94560,color:#fff,rx:12,ry:12
    style Hasura fill:#1a1a2e,stroke:#533483,color:#fff,rx:12,ry:12
    style RDS fill:#1a1a2e,stroke:#533483,color:#fff,rx:12,ry:12
    style S3 fill:#1a1a2e,stroke:#533483,color:#fff,rx:12,ry:12
    style GitHub fill:#1a1a2e,stroke:#533483,color:#fff,rx:12,ry:12
    style Synapse fill:#1a1a2e,stroke:#e94560,color:#fff,rx:12,ry:12
                ┌─────────────────────────────────────────────┐
                │              User's Browser                  │
                │  ┌─────────────────────────────────────────┐ │
                │  │  N.E.S.T. SPA (React + CesiumJS)       │ │
                │  │  - Cognito sign-in (idToken in store)   │ │
                │  │  - Sends Bearer JWT on ALL /api/* calls │ │
                │  └──────────┬──────────────────────────────┘ │
                └─────────────┼────────────────────────────────┘
                              │
                ┌─────────────▼──┐  ┌────────────────┐
                │ AWS Cognito    │  │   nest-api     │
                │ User Pool      │  │   CF Worker    │
                │ phenom-dev-    │  │                │
                │ local          │  │  Verifies JWT  │
                └────────────────┘  │  Forwards JWT  │
                                    │  to Hasura     │
                                    └────┬───────────┘
                                         │
                          ┌──────────────┼──────────────┐
                          │              │              │
                ┌─────────▼─────┐  ┌─────▼─────┐  ┌────▼──────┐
                │ Hasura GraphQL│  │  AWS S3   │  │ GitHub    │
                │ via Cloudflare│  │ (drop     │  │ App API   │
                │ → ALB         │  │  media)   │  │ (teams)   │
                └───────┬───────┘  └───────────┘  └───────────┘
                        │
                ┌───────▼───────┐
                │  PostgreSQL    │
                │  (AWS RDS)     │
                └────────────────┘

The Worker forwards the user's Cognito JWT to Hasura on every `/api/*` call. Both the Worker and Hasura verify the same JWT against Cognito JWKS independently -- no admin secret in the request path. All data (events, lists, shares, users, notifications) is served from Hasura/Postgres.

---

## Deployments

| Site | URL | Platform | Deploys From |
|------|-----|----------|-------------|
| **Production UI** | nest.thephenom.app | CF Pages (`nest-phenom`) | Merge to `main` via CI |
| **Production API** | nest-api Worker (prod) | CF Workers (`nest-api-prod`) | Merge to `main` via CI |
| **Dev UI** | dev-nest.thephenom.app | CF Pages (`dev-nest-phenom`) | Merge to `develop` via CI |
| **Dev API** | nest-api Worker | CF Workers (`nest-api`) | Merge to `develop` via CI |

### CI/CD Pipeline (`nest-ci.yml`)

All deploys are automated via GitHub Actions:

1. **Layer 1** -- Typecheck & Lint (all pushes + PRs to `admin_sandbox/**`)
2. **Layer 2** -- Unit Tests (depends on Layer 1)
3. **Dev Deploy** -- On merge to `develop`: deploys Worker + Pages to dev-nest
4. **Prod Deploy** -- On merge to `main`: deploys Worker (`--env production`) + Pages to nest


### Required DNS Records

If DNS records go missing, the service becomes unreachable. Verify these exist:

| Record | Type | Target | Proxied |
|--------|------|--------|---------|
| `nest.thephenom.app` | CNAME | `phenom-backend.pages.dev` | Yes |
| `dev-nest.thephenom.app` | CNAME | `dev-nest-phenom.pages.dev` | Yes |

**Note**: Worker Routes do NOT require separate DNS records -- they intercept requests on existing domains.

---

## Authentication

### AWS Cognito (Primary)

| Setting | Value |
|---------|-------|
| **Pool name (dev)** | `phenom-dev-local` |
| **Pool ID (dev)** | `us-east-1_AkG9mnbjA` |
| **Pool name (staging)** | `phenom-staging` |
| **Pool ID (staging)** | `us-east-1_n8gO6SbP6` |
| **Pool name (prod)** | `phenom-prod` |
| **Pool ID (prod)** | `us-east-1_knEL7cqS3` |
| **Region** | `us-east-1` |
| **JWKS URL** | `https://cognito-idp.us-east-1.amazonaws.com/{pool-id}/.well-known/jwks.json` |
| **Frontend SDK** | `amazon-cognito-identity-js` |
| **Worker verification** | `nest-api/src/auth.ts` → `verifyCognitoJwt()` |
| **Hasura verification** | `HASURA_GRAPHQL_JWT_SECRET` env var, points at the same JWKS |

The frontend client IDs are scoped to specific apps (`phenom-dev-hasura-client`, `phenom-dev-web-chat`, etc.) and visible in the AWS console under each pool's "App integration" tab.

The full request chain is documented in [the platform overview](https://int-docs.thephenom.app/docs/project/nest/#authentication-flow).

### Cloudflare Access (Login Gate)

CF Access **no longer gates** `nest.thephenom.app` or `dev-nest.thephenom.app`: those CF Access apps were deleted 2026-05-16, so **Cognito is the sole gate** for both (see the nest-access Cognito group). CF Access remains only on the `nest-firebase` snapshot, `int-docs.thephenom.app`, and `*.phenom-backend.pages.dev`. API routes use Cognito Bearer tokens; there is no `CF_Authorization` cookie auth.

| Setting | Value |
|---------|-------|
| **Team domain** | `thephenom-app.cloudflareaccess.com` |
| **IdP type** | GitHub OAuth App |
| **OAuth App Client ID** | `Ov23li8PtxSU0LskOBU9` |
| **OAuth App secret** | `pass sanmarcsoft/github-oauth/phenom-oauth-client-secret` |
| **Callback URL** | `https://thephenom-app.cloudflareaccess.com/cdn-cgi/access/callback` |
| **CF Account** | Pavestar (`pass cloudflare/phenom-account-id`) |

**Access Apps** (all use the same IdP):

| App | Domain | Policy |
|-----|--------|--------|
| nest-firebase snapshot | nest-firebase.thephenom.app | GitHub login required |
| The Phenom App Docs | int-docs.thephenom.app | GitHub login required |
| phenom-backend Pages | *.phenom-backend.pages.dev | GitHub login required |

> The CF Access apps for `nest`, `nest-prod`, and `dev-nest` were **deleted 2026-05-16**.
> `nest.thephenom.app` and `dev-nest.thephenom.app` are no longer behind CF Access; Cognito is
> their sole gate. Verified live 2026-05-28 (both serve the app directly, no Access wall).

### GitHub App (Team Data)

| Setting | Value |
|---------|-------|
| **App name** | N.E.S.T. Team |
| **App ID** | `3085870` |
| **Client ID** | `Iv23li5FmbWUV3ME5ZOt` |
| **Client secret** | `pass sanmarcsoft/github-oauth/phenom-nest-client-secret` |
| **Purpose** | Pull INT team member profiles from `Phenom-earth` GitHub org |
| **NOT used for** | CF Access login (that's the OAuth App above) |



CRITICAL: The OAuth App and GitHub App are DIFFERENT things. The OAuth App (Ov23li...) controls CF Access login. The GitHub App (Iv23li...) pulls team data. Never overwrite the CF Access IdP with GitHub App credentials – this breaks production login for ALL sites.

### Cognito User Pool (Unified Auth) N.E.S.T. and Synapse chat share a single Amazon Cognito user pool. Users authenticate once and access both services without re-entering credentials. | Setting | Value | |---------|-------| | **Pool name** | `phenom-prod` | | **User Pool ID** | `us-east-1_knEL7cqS3` | | **SPA client** | `phenom-prod-nest-spa` | | **SPA client ID** | `5vlgjrab90897c45ls9jkf9s2p` (public client, no secret) | | **Region** | `us-east-1` | | **Chat homeserver** | `chat.thephenom.app` (prod Synapse) | Both `VITE_COGNITO_USER_POOL_ID` and `VITE_COGNITO_CLIENT_ID` are hardcoded in the `layer-3-deploy-pages-prod` CI job and must match this pool. The `nest-api` Worker reads `COGNITO_USER_POOL_ID` from `wrangler.toml [env.production].vars`.

The prod Worker routes Hasura requests to api.thephenom.app/v1/graphql – NOT chat.thephenom.app. Cloudflare Workers fetching from any host on the same Cloudflare zone route back through the Worker itself, causing an intra-Worker loop. Always use api.thephenom.app as the Hasura endpoint for the prod Worker.

--- ## Data Sources ### Hasura GraphQL (Primary -- all N.E.S.T. data) | Environment | Endpoint | |-------------|----------| | **Production** | `https://api.thephenom.app/v1/graphql` | | **Staging/dev** | `https://api-staging.thephenom.app/v1/graphql` | | Setting | Value | |---------|-------| | **Database** | PostgreSQL on AWS RDS | | **Schema migrations** | `phenom-backend/hasura/migrations/default/` | | **Metadata** | `phenom-backend/hasura/metadata/` | | **Auth** | Cognito JWT (forwarded from the user, not an admin secret) | | **Hasura deploy workflow** | `deploy-hasura-development.yaml` (on PR merge to `develop`) | **Tables used by `nest-api`**: - **Events**: `phenom`, `drops`, `phenom_media` (via `/api/events`) - **Lists**: `lists`, `list_items` (CRUD via `/api/lists`) - **Sharing**: `list_shares`, `item_shares` (via `/api/shares`, `/api/lists/:id/share`) - **Users**: `users` (via `/api/users`, `/api/users/me`) - **Transcriptions**: `transcriptions` (cached video transcription/translation) **Other tables in the schema**: `phenom_coords`, `phenom_sensor_data`, `phenom_shoots`, `phenom_categories`, `phenom_behavior_types`, `behavior_types`, `categories`, `roles`. See `hasura/metadata/databases/default/tables/` for the full list. **Backfill from Firestore**: `phenom-backend/hasura/scripts/migrate_firebase_to_postgres.py` is the idempotent migration script (Cognito user resolution included). Re-runnable any time to catch new Firestore writes from the legacy Buzzard mobile app while it is being phased out. ### Firebase Firestore (Retired) | Collection | Source | Status | |------------|--------|--------| | `phenom` | Buzzard (legacy mobile app) | Data migrated to Hasura | | `phenom/{id}/shoots` | Buzzard | Data migrated to Hasura | | `drops` | Phenom Drop web app | Data migrated to Hasura | **Firebase Project**: `phenom-7ee1a` **Status**: `firebase.ts` has been deleted from nest-api. The `FIREBASE_SERVICE_ACCOUNT` Worker secret should be removed from Cloudflare. The backfill script can still be re-run to capture any final Buzzard writes during sunset. ### AWS S3 (Drop Media) | Environment | Bucket | URL format | |-------------|--------|------------| | **Production** | `phenom-prod-media-storage` | `https://phenom-prod-media-storage.s3.us-east-1.amazonaws.com/{s3Key}` | | **Dev/staging** | `phenom-dev-media-staging` | `https://phenom-dev-media-staging.s3.us-east-1.amazonaws.com/{s3Key}` | | Setting | Value | |---------|-------| | **Region** | `us-east-1` | | **Access** | Public GET for `uploads/*` prefix, restricted by Referer | **CORS Configuration** (`docs/s3-cors.json`) -- two rules as of 2026-03-14: **Rule 1 -- N.E.S.T. origins** (GET, HEAD): - `https://nest.thephenom.app` - `https://dev-nest.thephenom.app` - `https://phenom.matthewstevens.org` **Rule 2 -- Drop origins** (GET, PUT, HEAD; exposes `ETag` header): - `https://www.thephenom.app` - `https://thephenom.app` > Drop origins require PUT for presigned URL uploads and HEAD for integrity checks. The `ETag` exposed header is needed so the client can verify upload success. **Bucket Policy Referers** (`docs/s3-bucket-policy.json`): - `https://nest.thephenom.app/*` - `https://dev-nest.thephenom.app/*` - `https://phenom.matthewstevens.org/*`

Adding a new domain: If you deploy N.E.S.T. or Drop to a new domain, you MUST update the S3 CORS config (the appropriate rule for the site type) AND the bucket policy to include the new origin. N.E.S.T. domains need GET + HEAD; Drop domains need GET + PUT + HEAD. Without this, media loading or uploads will fail with CORS errors.

### Cloudflare D1 (Retired) D1 SQLite (`nest-db`, ID `c7929d36-0f8e-4343-8f18-99bee03e326e`) previously stored lists, shares, and users. All tables have been migrated to Hasura/Postgres via the `20260409100000_nest_tables` migration. The D1 binding has been removed from `wrangler.toml`. --- ## API Endpoints ### nest-api (Cloudflare Worker) Lives at: `admin_sandbox/nest-api/` (TypeScript, deployed via `wrangler deploy`). All routes require `Authorization: Bearer <Cognito JWT>`. The Worker verifies the JWT and forwards it to Hasura for row-level security. | Endpoint | Method | Purpose | |----------|--------|---------| | `/health` | GET | Health check (no auth) | | `/api/events` | GET | Phenom + drop events from Hasura, with presigned drop media URLs | | `/api/media/sign` | GET | Generate presigned S3 GET URL for an `uploads/*` key | | `/api/lists` | GET / POST | List or create user's curated lists | | `/api/lists/:id` | PATCH / DELETE | Rename or delete a list | | `/api/lists/:id/items` | GET / POST | Items in a list | | `/api/lists/:id/items/:itemId` | DELETE | Remove item from list | | `/api/lists/:id/share` | POST | Share list with team member | | `/api/lists/:id/share/:userId` | DELETE | Unshare list | | `/api/shares` | POST | Share individual item (phenom or drop) | | `/api/shares/inbox` | GET | Items shared with me | | `/api/shares/:id/seen` | PATCH | Mark share as seen | | `/api/users` | GET | All known users | | `/api/users/me` | GET / PATCH | Current user profile + notification counts | | `/api/notifications/count` | GET | Unseen share counts | | `/api/teams` | GET | GitHub org teams (requires GitHub App) | | `/api/teams/:slug/members` | GET | Team members | ### Drop Upload API (AWS Lambda) | Setting | Value | |---------|-------| | **Endpoint** | `https://lxd3gpzlph.execute-api.us-east-1.amazonaws.com/development/upload/generate-url` | | **Purpose** | Generate presigned S3 PUT URL for drop upload | | **Auth** | Upload password (server-side only) | ### Drop Backend (Python) | Setting | Value | |---------|-------| | **Repo** | `phenom-drop/backend/server.py` | | **Endpoints** | `/api/drop/send-password`, `/api/drop/verify-password`, `/api/drop/upload` | | **Purpose** | Email verification, hash registry, S3 upload proxy | --- ## Drop Upload Flow

User (phenom-drop web app) │ ├─1─▶ Verify C2PA credentials (client-side) │ ├─2─▶ POST /api/drop/send-password │ → Backend sends 6-char OTP via Brevo email │ ├─3─▶ POST /api/drop/verify-password │ → Backend validates OTP + registers hash │ ├─4─▶ POST /api/drop/upload │ → Backend proxies to AWS Lambda │ → Lambda returns presigned S3 PUT URL │ → Backend writes metadata to the drops table in Hasura/Postgres │ ├─5─▶ PUT to S3 presigned URL (direct upload from browser) │ → File stored at s3://phenom-dev-media-staging/uploads/… │ → S3 CORS Rule 2 allows PUT from Drop origins │ └─6─▶ Backend records mediaUrl + s3Key → N.E.S.T. reads drops from Hasura via /api/events → Worker presigns the s3_key on demand if mediaUrl is null


**Note** (2026-03-14): `computeFileHash()` in `www/web/drop.html` now uses streaming SHA-256 (chunked reads via `ReadableStream`) for files >100MB. This avoids loading the entire file into memory at once, preventing browser tab crashes on large uploads.

---

## Credential Locations (GPG Pass)

| Path | Purpose |
|------|---------|
| `cloudflare/phenom-account-id` | CF account ID (Pavestar) |
| `cloudflare/phenom-zone-id` | thephenom.app zone ID |
| `cloudflare/phenom-api-token` | CF API token (limited perms) |
| `verifieddit/CLOUDFLARE_API_KEY` | Global API Key (works across accounts) |
| `verifieddit/CLOUDFLARE_AUTH_EMAIL` | Auth email for Global API Key |
| `sanmarcsoft/github-oauth/phenom-oauth-client-id` | OAuth App for CF Access |
| `sanmarcsoft/github-oauth/phenom-oauth-client-secret` | OAuth App secret |
| `sanmarcsoft/github-oauth/phenom-nest-client-id` | GitHub App for team data |
| `sanmarcsoft/github-oauth/phenom-nest-client-secret` | GitHub App secret |

---

## CesiumJS Globe Configuration

### Imagery Layers (bottom to top)

| Layer | Provider | Visibility |
|-------|----------|-----------|
| Day imagery | Esri World Imagery (satellite) | `dayAlpha=1.0`, `nightAlpha=0.0` |
| Night imagery | NASA VIIRS Black Marble 2012 | `dayAlpha=0.0`, `nightAlpha=1.0` |
| Political borders | CartoDB dark_only_labels | Toggled via toolbar button |
| Google 3D Tiles | Google Maps Tile API | Hidden above 500km altitude |

### Google Maps Tile API

| Setting | Value |
|---------|-------|
| **API Key** | `AIzaSyDLi52bh9UXdiqZows69Z41S6qWnsPwzII` |
| **Endpoint** | `https://tile.googleapis.com/v1/3dtiles/root.json?key=...` |
| **Loaded via** | `Cesium3DTileset.fromUrl()` |
| **Night dimming** | `imageBasedLightingFactor` adjusted per frame by sun angle |

### Day/Night Effects

| Effect | Mechanism | Scope |
|--------|-----------|-------|
| Globe day/night | `globe.enableLighting = true` + `SunLight` | Base globe surface |
| Atmosphere | `dynamicAtmosphereLighting` + `dynamicAtmosphereLightingFromSun` | Sky limb |
| 3D Tile dimming | `imageBasedLightingFactor` per frame | Google 3D Tiles only |
| Night vision | CSS `hue-rotate + brightness + desaturate` filter | Entire canvas below 500km |
| Tile visibility | `tileset.show = height < 500km` | Reveals day/night imagery at global view |

### Clock

- Default: current real time (`JulianDate.now()`)
- On marker click: jumps to recording timestamp
- On deselect: resets to real time
- `shouldAnimate = false` -- time only changes on marker selection

---

## Build & Deploy Checklist

Before deploying any changes:

1. **Google Maps key** is hardcoded in `CesiumGlobe.tsx` (not an env var)
2. **S3 CORS** must include the deployment domain
3. **CF Access** must have an app + policy for the deployment domain
4. **OAuth App callback** must be `https://thephenom-app.cloudflareaccess.com/cdn-cgi/access/callback`
5. **Worker secrets** must be set via `wrangler secret put`: `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` (no admin secret needed -- all Hasura queries use the caller's Cognito JWT)
6. **GitHub environment** `Development` must have `HASURA_ENDPOINT` variable set to `https://api-staging.thephenom.app`

---

**Last Updated**: 2026-05-25