TV pairing
The television has no keyboard and no payment form. Pairing is how an account gets onto a TV: the TV shows a code, the phone joins it, and the TV adopts the account.
How a phone hands an account to a TV
Everything travels through one RTDB session; the TV trusts what it reads.
The same system, laid out automatically. Use the controls to zoom, open it full screen, or download it for a deck.
The protocol
The same six files exist in three copies:
gstv-tv/services/pairing-contract/greyscreen-expo-webview/services/pairing-contract/greyscreen-webview/src/services/pairing-contract/
Each header claims a canonical source synced by sync.sh; neither the source directory nor the
script exists. The copies are byte-identical except RTDB_URL. Treat any one as the contract,
and change all three together.
Paths
Root is tv-pairing.
| Function | Path |
|---|---|
sessions(sid) | tv-pairing/sessions/{sid} |
sessionStatus(sid) | .../status |
commands(sid) | .../commands |
command(sid, cid) | .../commands/{cid} |
tvState(sid) | .../tvState |
presence(sid, uid) | .../presence/{uid} |
userData(sid) | .../userData |
subscriptionData(sid) | .../subscriptionData |
Shapes
PairingSession {
id, tvDeviceId, tvDeviceName,
status, // waiting | paired | disconnected
mobileDeviceId?, mobileDeviceName?,
createdAt, expiresAt?,
userId?, userData?, subscriptionData?,
tvUid?, mobileUid?
}
PairingUserData { id, name, email, photoURL? }
PairingSubscriptionData { isValid, priority, subscriptionId?, dateEndSubscription? }
Codes use six characters from ABCDEFGHJKLMNPQRSTUVWXYZ23456789 — no I, O, 0 or 1.
SESSION_EXPIRY_MS is 30 minutes.
The sequence
- The TV creates
sessions/{code}withstatus: "waiting"and shows the code plus an unsigned QR whosepinequals the session ID. - The phone reads the session once and requires
status === "waiting". It does not checkexpiresAt. - The phone sets
status: "paired"and writesuserData. - The TV's value listener fires and applies the account locally, without server verification.
- Commands go to
commands/{sessionId}; the TV'schild_addedlistener parses, executes and removes the child as an acknowledgement.
The remote
REMOTE_ACTIONS declares navigate, select, play, pause, seek, volume, back,
search. The pure reducer handleCommand supports navigate.dpad, navigate.screen, select,
back, player.play, player.pause, player.seek, player.volume, search and noop.
What actually executes on the TV today: back and navigate.screen. Everything else is
published on RemoteEffectBus with no subscribers, so D-pad, select, play/pause, seek, volume
and search are dropped. This is a known gap, not a configuration problem.
tvState, presence and subscriptionData are modelled but never written.
Cast
Android only, using the default Google Cast receiver (CC1AD845). iOS AirPlay is a stub and
CastButton.ios is empty.
Why this is the weakest link
tv-pairing/sessions/$sessionId has .read: true, .write: true. Anyone who can reach the
database can enumerate sessions, write into them, and inject commands.
Combined with the missing server verification, the consequences are:
- Session enumeration — guess or list codes and read whatever is in the session.
- Command injection — write to
commands/{sessionId}; the TV will execute what it knows how to execute. - Impersonation — write
userDataorsubscriptionData; the TV adopts them. - Forged entitlement — the TV has no way to tell a real subscription from a typed one.
There is no server authority, collision control, rate limit or audit trail. Fixing it means mediating session creation and validation server-side, enforcing expiry, verifying user and subscription data before the TV applies it, and either implementing the missing command handlers or removing the remote feature.
Debugging a pairing failure
- Same database? The three copies differ only in
RTDB_URL. A mismatch looks like "invalid code". - Still waiting? A session that was already paired cannot be joined again.
- Expired? The phone ignores
expiresAt; the TV does not. An old code may look valid on the phone and be rejected on the TV. - Rules deployed? Admin SDK access bypasses rules, but the clients do not. A rules change
that tightens
tv-pairingwill break clients until they authenticate.
Next: Deployment topology — where all of this actually runs.