Skip to main content

Architecture

GreyScreen is a set of small services behind one gateway. The gateway owns TLS and path routing; the services own one job each; Firebase owns shared state. Two flows are easy to miss: artwork is converted inside the admin API, and the admin learns the outcome of an encode over SSE, not by polling.

Switch between Explained and Technical below. Both maps are live: hover to isolate connections, click a node for its role and key files, trace a flow, zoom, open full screen, and download the map as SVG or PNG.

GreyScreen · global architecture

The platform, service by service

Every runtime component, the protocol between them, and the flows that matter. Click a node for its role, stack and key files.

Trace a flow
CLIENTSEDGESERVICESDATA & COMPUTEEXTERNALWEBgstv-websitePublic streaming siteReact 18 · Vite 3WVBgreyscreen-webviewIn-app streaming SPAReact 18 · Vite 5APPgreyscreen-expo-webviewPhone shell · ExpoExpo 54 · RN 0.81TVgstv-tvAndroid TV clientExpo 53 · RN TVADMgstv-admin-webBack-office SPAReact 18 · Ant Design 5GWvps-reverse-proxyMulti-domain nginx gatewaynginx · TLS · :80/:443APIgstv-customer-apiPublic APIExpress 4 · :3000OPSgstv-admin-apiAdmin API + orchestrationExpress 4 · :3003STRgstv-multimediaHLS streaming + cacheNMS · :8000 / :8080FSFirestoreSource of truthgrey-screen-tv · nam5RTDBRealtime DatabaseQueues + pairinggrey-screen-tv-default-rtdbIMGGCS · imagesPosters + WebP artworkgstv-images-<env>INGCS · inputRaw master filesgstv-videoinput-<env>OUTGCS · outputHLS renditionsgstv-videos-<env>VPSgstv-vps workerEphemeral ffmpeg VMGCE · self-deletesAUTHFirebase AuthGoogle / Apple identityID tokensFCMFirebase Cloud MessagingPush topicsusers · videos · tv-usersSTRStripeCards + subscriptionshosted + embeddedFLWFlutterwaveMobile moneyproduction merchant
Zoom 100 percent

The shape in five decisions​

1. One public surface​

vps-reverse-proxy is a two-tier nginx gateway: a TLS gateway on :80/:443, and one proxy per domain that matches the longest path prefix and strips it before forwarding. The upstream container therefore sees /catalog/home, not /api/customer/catalog/home.

Public pathUpstreamPrefix stripped
/gstv-websiteno
/webview/greyscreen-webviewyes
/admin/gstv-admin-webyes
/api/customer/gstv-customer-api:3000yes
/api/admin/gstv-admin-api:3003yes
/stream/gstv-multimedia:8000yes

2. Static where possible​

The website, webview and admin SPA are static nginx images with an SPA fallback and /healthz. They hold no server secrets. The phone shell and TV app are built with EAS and are not part of the Docker pipeline.

3. Two APIs, one database​

gstv-customer-api is read-heavy and public. gstv-admin-api is internal and orchestration-heavy. They share one Firestore database, so a schema change in either is a contract change for the other API and for every client. Firestore document IDs are API contracts: _id is synthesised from the document ID, and plans and admins keep their legacy Mongo _id strings.

gstv-admin-api must run as exactly one replica because its RTDB consumers are child_added claim queues with no lease. The notification hub is the exception — it is a fan-out log — but the process cannot scale until the claim consumers get a lease.

4. The catalogue is a live snapshot​

gstv-customer-api subscribes to videos, series and seasons where isReady == true and keeps the result in memory. It re-subscribes with backoff. Readiness is set by a Firestore transaction in the admin API only after the transcode is complete, the trailer has a resolution, and the artwork exists. There is no cache to invalidate; the snapshot just changes.

Only aggregate view counts are cached (Top 10, five minutes). The ranked board is rebuilt from the live catalogue on every request, so a deleted or unpublished title cannot linger.

5. Uploads bypass the API​

Browsers upload directly to Cloud Storage using signed PUTs or resumable session URIs. The API signs, verifies and records; it does not proxy bytes. Artwork is converted to WebP synchronously during completeUpload, so the response already describes the final image.

The same system in Mermaid​

Mermaid produces a second, automatically laid-out reading from the same component model. It is useful for a slide, a review, or a quick sanity check that the service list is still complete.

Auto-laid out with Mermaid, rendered live in the page.

Same platform · Mermaid

The same story, drawn by Mermaid

A second view of the platform for readers who prefer a simpler, box-and-arrow map. Every block opens a plain-language card.

Rendering Mermaid diagram…
Zoom 100 percent
Mermaid

The same system, laid out automatically. Use the controls to zoom, open it full screen, or download it for a deck.

How the pieces connect​

EdgeProtocolContract
Browser → gatewayHTTPSHost + path; TLS terminated at the gateway
Gateway → SPAsHTTPPrefix stripped; SPA fallback; /healthz
Gateway → APIsHTTPJSON REST; /health; Bearer token or admin cookie
Gateway → multimediaHTTPHLS playlists and segments under /stream/
Customer API → FirestoreAdmin SDKLive onSnapshot on ready catalogue docs
Admin API → FirestoreAdmin SDKCRUD, staging, readiness transaction, notifications
Admin API ↔ workerRTDBqueue/transcode, queue/resolutionEvents, vpsStatus
Worker → bucketsgcsfuseRead masters from input, write HLS to output
Multimedia → outputgcsfuseRead HLS; cache in RAM then disk
Stripe / Flutterwave → customer APIHTTPS webhookSignature or verif-hash; dedupe where implemented
Admin API → admin SPASSE/notifications/stream, cookie auth, Last-Event-ID replay

Reading paths​