Infrastructure & Service Map
Sensitive Infrastructure: This page documents credential locations and service endpoints. Access is restricted to INT team members via Cloudflare Access.
Service Architecture
┌─────────────────────────────────────────────┐
│ User's Browser │
│ ┌─────────────────────────────────────────┐ │
│ │ N.E.S.T. SPA (React + CesiumJS) │ │
│ │ - 3D Globe with Google Photorealistic │ │
│ │ - Marker rendering (UFO/airplane SVG) │ │
│ │ - Day/night imagery blending │ │
│ └──────────┬──────────┬──────────┬────────┘ │
└─────────────┼──────────┼──────────┼──────────┘
│ │ │
┌─────────────▼──┐ ┌────▼────┐ ┌─▼──────────┐
│ Cloudflare │ │Firebase │ │ nest-api │
│ Access (OAuth) │ │Firestore│ │ (CF Pages │
│ │ │ │ │ Functions)│
└────────────────┘ └────┬────┘ └─────┬──────┘
│ │
┌──────────────┴──┐ ┌────▼─────┐
│ AWS S3 │ │ CF D1 │
│ (Drop media) │ │ (Lists, │
│ │ │ Users) │
└──────────────────┘ └──────────┘
Deployments
| Site | URL | Platform | Deploys From |
|---|---|---|---|
| Production | nest.thephenom.app | Netlify | main branch auto-deploy |
| Dev | dev-nest.thephenom.app | CF Pages (dev-nest-phenom) | wrangler pages deploy from ai |
| Dev (direct) | *.dev-nest-phenom.pages.dev | CF Pages | Same — per-deploy preview URLs |
Deployment Commands
# Build on ai.matthewstevens.org (Apple Silicon, has Node + npm)
ssh ai "cd .../admin_sandbox/phenom && npx vite build"
# Deploy to dev-nest CF Pages
wrangler pages deploy dist --project-name=dev-nest-phenom --branch=main
# Production deploys automatically via Netlify on push to main
Authentication
Cloudflare Access (Login Gate)
| 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 | nest.thephenom.app | GitHub login required |
| Dev NEST Dashboard | dev-nest.thephenom.app | GitHub login + IP 84.112.12.104 |
| The Phenom App Docs | int-docs.thephenom.app | GitHub login required |
| phenom-backend Pages | *.phenom-backend.pages.dev | GitHub login required |
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.
Data Sources
Firebase Firestore
| Collection | Source | Data |
|---|---|---|
phenom | Phenom mobile app | Video recordings with GPS, timestamps, owner info |
phenom/{id}/shoots | Phenom mobile app | Individual recording segments with video URLs |
drops | Phenom Drop web app | Image submissions with C2PA, GPS, sensor data |
Firebase Project: phenom-7ee1a
Auth: Email/password service account
Credentials: .env file (VITE_FIREBASE_AUTH_EMAIL, VITE_FIREBASE_AUTH_PASSWORD)
Must be baked into build: Yes — Vite replaces import.meta.env.VITE_* at build time
AWS S3 (Drop Media)
| Setting | Value |
|---|---|
| Bucket | phenom-dev-media-staging |
| Region | us-east-1 |
| URL format | https://phenom-dev-media-staging.s3.us-east-1.amazonaws.com/{s3Key} |
| 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.apphttps://dev-nest.thephenom.apphttps://phenom.matthewstevens.org
Rule 2 — Drop origins (GET, PUT, HEAD; exposes ETag header):
https://www.thephenom.apphttps://thephenom.app
Drop origins require PUT for presigned URL uploads and HEAD for integrity checks. The
ETagexposed 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 (Lists & Users)
| Setting | Value |
|---|---|
| Database name | nest-db |
| Database ID | c7929d36-0f8e-4343-8f18-99bee03e326e |
| Tables | users, lists, list_items, list_shares, item_shares, transcriptions |
| Accessed by | Pages Functions at /api/* |
| Binding | DB in CF Pages project settings |
Schema: admin_sandbox/nest-api/schema.sql
API Endpoints
nest-api (CF Pages Functions)
Lives at: admin_sandbox/phenom/functions/api/[[path]].js
All endpoints require CF_Authorization cookie (CF Access JWT).
| Endpoint | Method | Purpose |
|---|---|---|
/api/health | GET | Health check (no auth) |
/api/lists | GET | List all user’s owned lists |
/api/lists | POST | Create new list |
/api/lists/:id | PATCH | Rename list |
/api/lists/:id | DELETE | Delete list |
/api/lists/:id/items | GET | Get items in a list |
/api/lists/:id/items | POST | Add item to list |
/api/lists/:id/items/:itemId | DELETE | Remove item |
/api/lists/:id/share | POST | Share list with team member |
/api/shares | POST | Share individual item |
/api/shares/inbox | GET | Get items shared with me |
/api/shares/:id/seen | PATCH | Mark share as seen |
/api/users | GET | List all users |
/api/users/me | GET | 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 Firestore `drops` collection
│
├─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 updates Firestore with mediaUrl + s3Key
→ N.E.S.T. PhenomFetcher reads this to display image
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:
- Firebase .env must exist on build server (ai):
VITE_FIREBASE_AUTH_EMAIL,VITE_FIREBASE_AUTH_PASSWORD - Google Maps key is hardcoded in
CesiumGlobe.tsx(not an env var) - S3 CORS must include the deployment domain
- D1 binding must be configured in CF Pages project settings
- CF Access must have an app + policy for the deployment domain
- OAuth App callback must be
https://thephenom-app.cloudflareaccess.com/cdn-cgi/access/callback
Last Updated: 2026-03-14
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.