Deployment topology
GreyScreen runs on two hosts — a VPS and Google Cloud — plus four external providers. Nothing runs on a developer's machine except a browser, and the only component that appears and disappears on its own is the transcoding worker.
Where each piece runs
The gateway is the only public door; the worker is the only ephemeral machine.
The same system, laid out automatically. Use the controls to zoom, open it full screen, or download it for a deck.
The VPS
One nginx gateway per VPS, one branch per VPS, and one Compose stack underneath.
| Layer | What it is |
|---|---|
| TLS tier | Terminates TLS, one certificate per domain |
| Domain proxies | One proxy per domain, longest-prefix match, prefix stripped |
| Containers | Website, webview, admin web, customer API, admin API, multimedia |
| Config source | domains.json, then generate.sh, then reconcile.sh |
The test VPS is 82.38.44.60. Production hosts have their own branch of
vps-reverse-proxy — the repository is deployed per host, not per environment variable.
Adding an endpoint to a repo changes nothing publicly. The route must exist in domains.json
and generate.sh must run. This is the most common "it works locally" explanation.
Google Cloud
Everything stateful lives in one project, grey-screen-mobile-and-web-app:
| Service | Role |
|---|---|
Firestore (nam5) | Source of truth |
| Realtime Database | Queues, worker status, pairing |
| Cloud Storage | Three buckets per environment: images, input, output |
| Firebase Auth | Website and native sign-in |
| Firebase Cloud Messaging | Push tokens stored on the user document |
| Compute Engine | The on-demand worker VM |
| Artifact Registry | The worker image the instance template pulls |
The worker is the only machine that is created on demand. It is not a managed service, not a
cluster, and not autoscaled: a scheduler in the admin API creates or deletes one fixed-name
instance, gstv-vps-<env>-worker, based on whether a queue is non-empty.
Two orchestrators, one project
Test and production each have an admin API that can create worker VMs. They are safe together only because:
- each VM carries a
labels.envoftestorprod; - each scheduler filters on its own label and re-checks client-side before acting.
Never remove either guard. Two workers on one queue would double-process every job.
Boot and shutdown of the worker
- The scheduler sees a non-empty
queue/transcode(or the legacyqueue/imageJobs). - It inserts an instance from the per-environment template.
startup-script.shasserts metadata, env identity, bucket names, the baked Firebase key, the RTDB host and the timers.- It pulls the image, mounts input and output with gcsfuse, starts the container.
- The worker drains the queue, then deletes its own VM after five idle minutes, or at the 24-hour hard cap regardless of work.
A vpsStatus/heartbeat record naming a VM that no longer exists is normal. It ages out at
VPS_HEARTBEAT_STALE_MS (default 60 s) and must not be read as "a VM is running".
Images and drift
The worker has three image references in the wild:
| Reference | Built by |
|---|---|
docker.aigenhub.us/gstv-vps-test:test | CI on test |
docker.aigenhub.us/vps:prod | CI on main, no deploy job |
us-central1-docker.pkg.dev/.../gstv-vps:<env> | The instance template pulls this; no repo builds it |
This is the single most confusing part of the platform. The test path is coherent. The production path is not: the image the VM actually pulls is not the image CI pushes.
Secret Manager secrets exist but the boot script never fetches them; it validates an image-baked Firebase key and uses application default credentials for gcsfuse.
Native clients
The phone shell and TV app ship through EAS, not through the VPS.
| Client | Distribution | Firebase identity |
|---|---|---|
| Phone shell | EAS profiles; Android from android, iOS from ios | Selected by build variant in app.config.ts |
| Android TV | EAS + Play | Same variant pattern; google-play-key.json is referenced but missing |
There is no Docker or server deploy for either. A native release is independent of a VPS deploy, which is why a client bug can outlive a backend rollback.
Public path layout
| Path | Upstream |
|---|---|
/ | website |
/webview/ | webview SPA |
/admin/ | admin SPA |
/api/customer/ | customer API |
/api/admin/ | admin API |
/stream/ | multimedia |
Everything else on a host is routed by domains.json.
What "deploy" means per repository
| Repository | Test | Production |
|---|---|---|
| Website, webview, admin web, both APIs | :test image, SSH + flock | :prod image, GitHub production environment |
| Worker | Compose vps profile, usually off | Manual; no deploy job |
| Multimedia | Auto-deploys on test with no gate | Separate path |
| Native apps | EAS preview | EAS production + store |
| Gateway | Push to a non-main branch | Push to the VPS branch |
Deploy order: if a change touches both the worker and the API, deploy the worker image first.
Next: Build and deploy.