Skip to main content

Data stores

Three stores, three jobs, no overlap:

StoreJobNever used for
FirestoreSource of truth: users, catalogue, money, auditQueues, ephemeral state
Firebase Realtime DatabaseQueues, worker status, pairing sessionsA second catalogue
Google Cloud StorageBytes: artwork, masters, HLS outputQueryable metadata
Map · data

What is stored where

Firestore owns truth, RTDB owns movement, Cloud Storage owns bytes.

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.

Firestore​

Constants live in src/db-core/paths.ts; helpers in src/db-core/doc.ts (toApiDoc synthesises _id from the document ID and drops __v). Region nam5, native mode.

CollectionDoc IDNotes
users32-hex idProfile, firebaseUid, Stripe fields, fcmTokens, points, activeUntil
emailslowercased email{userId} — login lookup and unique-email guard
videoslegacy idMetadata, counters, isReady, readinessState, transcode, deletion, searchTokens[]
serieslegacy idtotalSeasons, totalEpisodes, isReady
seasonsseasonDocId(seriesId, seasonNumber)seriesId, seasonNumber, totalEpisodes, isReady
planslegacy Mongo _idpriority, prices, features; _id preserved in responses
paymentsauto IDProvider reference, amount (number in new writes), method, date
videoFeedbackauto ID{eventName, videoId, date, deviceId, duration, progression}
adminslegacy ObjectId stringbcrypt password, role, isActive. The doc ID keeps old JWTs valid.
auditLogsauto ID{adminId, adminEmail, action, targetType, targetId, details, ip, userAgent, at}
stagingfileId from addVideoUpload progress, per-kind image state, expiresAt +7 days
notificationsauto IDAdmin bell log; append-only, no TTL
paymentRefsFlutterwave txRefCorrelation record, expiresAt +7 days
webhookEventsprovider event keyCreate-only dedupe, expiresAt +24 hours
meta/bootstrapbootstrapGates one-time seeding of plans and admins
MyListfirebaseUidCanonical items[{videoId, addedAt}]; reads also merge legacy movies[]
viewEvents, viewDedupauto / ${firebaseUid}_${videoId}Top-10 aggregation; 30-minute dedupe

User subcollections​

PathDoc ID
users/{uid}/likes, dislikes, views, shares, downloadsvideoId
users/{uid}/playlists/{name} and .../videos/{videoId}playlist name, videoId
users/{uid}/subscriptions/{autoId}auto
users/{uid}/devices/{deviceId}deviceId
users/{uid}/pointsHistory, locationsauto
users/{uid}/progress/{videoId}videoId

Two rules that break builds​

Omit absent optional fields

The Firestore client is never configured with ignoreUndefinedProperties. A write that passes an undefined optional field throws. Build the payload without the absent keys, as the signup path does. Adding the setting is a deliberate cross-API change, not a local fix.

Document IDs are API contracts

_id is synthesised from the document ID. The videos, series, plans and admins collections deliberately keep legacy Mongo IDs. Re-keying a document changes every URL and invalidates every issued admin JWT.

Indexes and TTL​

Eight composite indexes cover season/series/episode queries on videos and seasons, plus a videos.videoId field override for collection-group queries. TTL policies for staging, paymentRefs and webhookEvents are still outstanding, and notifications has none — it grows without bound.

Realtime Database​

RTDB is the control plane. Admin SDK access bypasses rules; client rules deny queue, vpsStatus and flags.

PathWriterReader
queue/transcode/{pushId}admin API saveVideoworker
queue/transcodeFailed/{pushId}workeradmin API consumer, ops
queue/resolutionEvents/{pushId}workeradmin API consumer
queue/resolutionEventsFailed/{pushId}admin APIops
queue/imageJobs, imageJobsFailed, imageEventslegacy onlyworker image lane, admin API
vpsStatus/processingworkeradmin API (a string "true"/"false", not a boolean)
vpsStatus/currentVideo, currentImageJobworkerops
vpsStatus/heartbeatworkerdashboard, ops
vpsStatus/idleSince, shutdownPendingworkeradmin API
flags/shutdownCanceladmin API on enqueueworker
tv-pairing/sessions/{code}TV, phone, webviewTV, phone, webview

Every queue is a claim queue: consumed with child_added, removed after successful handling. There is no lease. Two consumers on one queue process everything twice — which is why the admin API is single-instance.

Dead letters (*Failed) are retained up to 100 entries and are what the notification feed turns into video.failed messages.

Cloud Storage​

Three buckets per environment, differing only by the -test / -prod suffix. Both APIs validate the suffix against APP_ENV at boot.

BucketHoldsConsumed by
imagesArtwork. Sources are converted to WebP at ingest at <prefix>/<fileId>.webp; sources are deleted after conversion.Admin web, clients via thumbnailUrl
inputRaw masters uploaded by the browser. Worker reads, then deletes.Worker
outputHLS ladder per title and per rendition, plus trailer renditions.gstv-multimedia through gcsfuse

The worker and the streaming server see the buckets as directories thanks to gcsfuse. That is the reason both containers need /dev/fuse and SYS_ADMIN, and the reason a local npm start without a mount does not behave like production.

Media and metadata stay separate​

A video's metadata and its bytes are joined by one string: the title's id.

  • Firestore says which rungs exist (transcode), and whether the title is live.
  • GCS holds the actual playlists and segments under <videoId>/<resolution>/.
  • The URL contract (/stream/videos/<videoId>/<resolution>/playlist.m3u8) is the only place the two meet.

That is why deleting a title is a cascade across all three stores, and why a partial delete leaves orphan bytes rather than a broken document.

Next: Notifications — how a background outcome reaches a human.