Skip to main content

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.

Flow · pairing

How a phone hands an account to a TV

Everything travels through one RTDB session; the TV trusts what it reads.

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.

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.

FunctionPath
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​

  1. The TV creates sessions/{code} with status: "waiting" and shows the code plus an unsigned QR whose pin equals the session ID.
  2. The phone reads the session once and requires status === "waiting". It does not check expiresAt.
  3. The phone sets status: "paired" and writes userData.
  4. The TV's value listener fires and applies the account locally, without server verification.
  5. Commands go to commands/{sessionId}; the TV's child_added listener 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.

World-readable, world-writable

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 userData or subscriptionData; 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​

  1. Same database? The three copies differ only in RTDB_URL. A mismatch looks like "invalid code".
  2. Still waiting? A session that was already paired cannot be joined again.
  3. Expired? The phone ignores expiresAt; the TV does not. An old code may look valid on the phone and be rejected on the TV.
  4. Rules deployed? Admin SDK access bypasses rules, but the clients do not. A rules change that tightens tv-pairing will break clients until they authenticate.

Next: Deployment topology — where all of this actually runs.