Scenario #73

Realtime system

Design a Realtime Presentation Remote Without a Custom Backend

Turn a phone into a live remote for the NicksLab Presentation Viewer. Scan one temporary QR code, then control slides, Spotlight, Laser, Draw, and Timer in realtime—without uploading the presentation.

Presenter

Laptop / browser

Controller

One phone

Coordinator

Firebase RTDB

Slide uploads

None

The experience

Two browsers become one presentation system

The laptop owns the presentation. The phone acts as a temporary input device. Firebase carries only the narrow coordination channel between them.

Presenter screen

System Design
Spotlight · Timer · Remote
realtime

Phone remote

02 / 0584 ms
PREVNEXT
touchpad
SpotlightTimer
01Upload slides
02Create Remote
03Scan QR
04Control presentation

Before the solution

The QR is the easy part

The real work is preserving identity, ownership, privacy, bounded capacity, and predictable recovery across two browsers that can disappear at any moment.

01Discovery

How does a phone find exactly one live presentation?

02Authentication

How can two browsers pair without creating user accounts?

03Authorization

What prevents an unknown visitor from sending commands?

04Concurrency

What prevents two presenters from claiming the same limited slot?

05Reliability

Who releases capacity when a browser crashes or disappears?

06Synchronization

How can timers and pointer movement feel live across different devices?

“Without a custom backend” means managed coordination—not the absence of a backend.

User journey

What happens from click to cleanup

Start with what the presenter and phone experience. Each visible step introduces one small system guarantee behind it.

A / Create

Presenter

  1. 01Upload slides

    The presentation and drawings remain inside the presenter browser.

  2. 02Open Remote

    Firebase is loaded lazily; an ordinary viewer consumes no realtime connection.

  3. 03Create identity

    Anonymous Auth gives the presenter an owner UID for this session.

  4. 04Create proof

    The browser generates a session ID, a 256-bit secret, and its SHA-256 hash.

  5. 05Claim capacity

    A transaction leases one empty or expired slot from 00 through 29.

  6. 06Show the QR

    The dialog publishes the controller link and enters Waiting for phone.

B / Pair

Phone

  1. 07Scan the QR

    The phone opens a controller route containing the slot, session, and fragment secret.

  2. 08Read the fragment

    Phone JavaScript reads #key locally; the initial Vercel request never receives it.

  3. 09Create phone identity

    The controller signs in anonymously and hashes the supplied secret.

  4. 10Verify the proof

    Rules check the session, expiry, and proof before private state is readable.

  5. 11Bind one controller

    An atomic write accepts the first valid phone UID and rejects every later phone.

  6. 12Show Connected

    Both devices subscribe and move from waiting to a live controller state.

C / Control

Both devices

  1. 13Send intent

    A tap becomes a typed command with a unique ID—not a direct slide mutation.

  2. 14Authorize the write

    Firebase Rules allow commands only from the bound controller UID.

  3. 15Apply it once

    The presenter validates the command, ignores duplicate IDs, and updates local state.

  4. 16Publish a snapshot

    The phone receives slide count, active tool, timer anchor, and connection status.

  5. 17Stream movement

    Spotlight and Laser use normalized, clamped, throttled coordinates.

  6. 18Measure health

    Ping, acknowledgement, presence, and a rolling median reveal connection quality.

D / Finish

Lease

  1. 19Renew while active

    A 30-second presenter heartbeat extends the 60-minute expiry.

  2. 20Detect disappearance

    onDisconnect updates presence when a browser closes unexpectedly.

  3. 21Disconnect phone

    Only the controller binding is released, allowing the same presenter to pair again.

  4. 22End Remote

    The session and capacity slot are removed immediately.

  5. 23Expire abandonment

    If cleanup cannot run, TTL makes the stale lease reclaimable.

System boundary

Three actors, one narrow channel

Vercel delivers the two Next.js clients. Firebase provides identity, transactions, realtime state, presence, and authorization.

Presenter

owns slides + executes commands

Firebase

identity + coordination + presence

Phone

sends intent + measures RTT

Vercel serves the pages · Firebase carries the live coordination

Firebase may know

Session ID, anonymous UIDs, expiry, command payloads, minimal viewer state, presence, and measured latency.

Firebase never receives

Presentation images, filenames, drawing canvases, camera video, or the content shown on the slide.

presentationRemoteSlots/
  00/
    sessionId, ownerUid, expiresAt

presentationRemoteSessions/
  00/
    ownerUid, controllerUid, pairingHash
    presenterConnected, controllerConnected
    viewerState/
    command/
    latencyPing/
    latencyAck/

QR anatomy

The QR points to a controller and carries temporary proof

It contains connection credentials, not presentation content. The original secret pairs the phone; only its SHA-256 hash is persisted.

Example presentation remote pairing QR code
Waiting for phone

What this QR contains

01

Controller route

Opens the mobile Presentation Remote page.

02

Capacity slot

07 · identifies which temporary lease to inspect.

03

Session identity

session-a8f2 · targets this exact presentation session.

04

Pairing secret

#key=… · proves the phone received the temporary link.

Connection information only. The presentation itself is never encoded into the QR.

256-bit secret

generated locally

SHA-256 hash

stored remotely

First phone UID

bound once

nickslab.app/playground/presentation-remote/07/session-a8f2#key=temporary-secret

07

Capacity slot

Which one of thirty leases the controller should inspect.

session-a8f2

Session identity

Stops an old link attaching to a newer lease in the same slot.

#key=…

Pairing credential

Read by phone JavaScript and compared through its stored hash.

Initial server request

/07/session-a8f2

Phone JavaScript can read

/07/session-a8f2#key=temporary-secret

Not inside the QR: slides, filenames, drawings, camera data, timer history, or personal account information. Anyone who copies the QR before pairing still holds a temporary credential, so it remains short-lived and visible only while waiting.

Concurrency

Capacity is a lease, not a counter

Thirty numbered slots create a hard application limit. A transaction decides who owns each slot.

Session lease capacity

One presenter + one phone per occupied slot.

40

expected connections

20/30 leases60 connections reserved as headroom

Unsafe: read, then write

Two presenters can observe the same empty slot and both believe they claimed it.

Safe: transact on the slot

Firebase commits one owner; every competing browser retries against the new value.

runTransaction(slotRef, current => {
  if (!current || current.expiresAt <= serverNow) {
    return { sessionId, ownerUid, expiresAt }
  }
  return undefined // occupied: abort
              })

Protocol

Send typed intent, not arbitrary mutations

The phone asks for an action. The presenter remains the authority that applies it to local presentation state.

{
  id: "cmd_next_7f2a",
  type: "next_slide",
  payload: undefined,
  issuedBy: "controller_uid"
}
Delivery ruleProcess once by ID
Navigationprevious_slide · next_slide
Pointertool_set · pointer_move · spotlight_set · spotlight_move
Drawingdraw_start · draw_move · draw_end · undo · redo · clear
Timertimer_start · timer_pause · timer_reset
Viewguides · scale · brightness · radius · blackout · theme

The honest trade-off: a single latest-command register is ideal for transient pointer positions. If every rapid discrete command must be guaranteed, use an append-only command log plus acknowledgements.

Coordinates

Transport proportions, never pixels

PHONE TOUCHPAD

PRESENTER VIEWPORT

x = clamp((pointerX - padLeft) / padWidth, 0, 1)
y = clamp((pointerY - padTop) / padHeight, 0, 1)

screenX = x * presentationWidth
screenY = y * presentationHeight

Pointer movement is throttled to roughly fifteen updates per second. Intermediate pointer positions may be discarded; drawing start and end boundaries may not. Drawings remain local and separate for every slide.

Clock discipline

Synchronize anchors; measure round trips

Presenter timer

Publish running state, mode, base seconds, and a Firebase-adjusted start timestamp. Each screen derives the visible second locally instead of writing once per second.

visible = base + (serverNow − startedAt)

Round-trip latency

The phone starts its own monotonic clock, sends a ping, waits for the matching acknowledgement, then reports the median of its latest five samples.

72 ms· median of five · online
PhonePing / AckPresenter

Security boundary

Coordinate the remote without collecting the presentation

Authentication identifies each browser. Deny-by-default Realtime Database Rules decide what that identity may read or change.

Temporarily stored

  • — Anonymous presenter and controller UIDs
  • — Slot ID, session ID, and expiry
  • — Pairing-secret hash
  • — Latest command and minimal viewer state
  • — Presence and latency records

Never uploaded

  • — Slide images or presentation content
  • — Presentation filenames
  • — Drawing canvases and strokes
  • — Camera or video stream
  • — Persistent personal account data
Public Firebase configuration identifies the project. Security Rules—not hidden client configuration—authorize access.
OperationPresenterPaired phoneOther visitor
Claim or release slot
Read private session
Bind controller UID
Write command
Write viewer state
Acknowledge latency

Rules also bound command IDs, allow-list command types, clamp coordinates, validate timestamps near server time, restrict numeric settings, and reject unknown payload fields. Client checks improve feedback; database rules provide security.

Recovery

Every failure deserves a specific state

FailurePresenterPhoneRecovery
Firebase configuration missingRemote unavailableConfiguration errorConfigure and redeploy
All thirty slots occupiedCapacity reachedNo controller linkTry again later
Invalid or expired QRStill waitingInvalid sessionScan a fresh QR
A second phone pairsFirst phone staysAlready pairedDisconnect the first
Presenter disconnectsViewer remains localPresenter offlineReconnect presenter
Phone disconnectsWaiting for phoneReconnect or rescanReuse the session
Old database rulesCommand not appliedDeployment warningDeploy current rules

Do not collapse these into “Something went wrong.” The recovery action is different: retry, wait, rescan, reconnect, or deploy the current rules.

Decision boundary

What changes beyond a portfolio-scale system?

The current architecture is intentionally sized for temporary one-presenter, one-phone sessions—not treated as a universal realtime design.

ConcernCurrent designAt larger scale
InfrastructureFirebase SparkPaid Firebase or dedicated realtime service
Capacity30 fixed leasesDynamic distributed admission control
ControllersOne phoneMultiple controllers with scoped roles
CommandsLatest-command registerDurable ordered stream with acknowledgements
IdentityAnonymous session UIDsAccount and organization membership
HistoryTemporary coordinationAudit log, analytics, and retention policy

Your turn

Defend the decisions

Answer each before opening the implementation. A strong answer names the invariant and the failure it prevents.

01Why is the pairing secret placed in the URL fragment instead of the pathname?Security

Trace identity, shared state, failure timing, and the exact invariant before choosing a mechanism.

02What race occurs when slot allocation uses a separate read followed by a write?Concurrency

Trace identity, shared state, failure timing, and the exact invariant before choosing a mechanism.

03Why are onDisconnect, heartbeat, and TTL all required?Reliability

Trace identity, shared state, failure timing, and the exact invariant before choosing a mechanism.

04Which commands may discard intermediate updates, and which must never be lost?Protocol

Trace identity, shared state, failure timing, and the exact invariant before choosing a mechanism.

05Why is an anchor timestamp more reliable than writing the timer every second?Time

Trace identity, shared state, failure timing, and the exact invariant before choosing a mechanism.

06Why should round-trip time be measured on one device with performance.now()?Latency

Trace identity, shared state, failure timing, and the exact invariant before choosing a mechanism.

07What prevents a random Firebase user from writing a valid command?Security

Trace identity, shared state, failure timing, and the exact invariant before choosing a mechanism.

08What changes when the product grows from 30 to 1,000 presentation pairs?Scaling

Trace identity, shared state, failure timing, and the exact invariant before choosing a mechanism.

Field notes

Remember this

  1. 01No custom backend does not mean no backend; Firebase is the managed coordinator.
  2. 02Put temporary bearer secrets in URL fragments and store only their hashes.
  3. 03Claim limited capacity with an atomic transaction, never read-then-write.
  4. 04Treat presence as a lease backed by disconnect hooks, heartbeats, and expiry.
  5. 05Normalize coordinates and throttle transient movement before transporting it.
  6. 06Use unique command IDs so reconnects cannot repeat discrete actions.
  7. 07Synchronize timers from an anchor; do not publish every visible second.
  8. 08Measure RTT on one monotonic clock and smooth it with a rolling median.
  9. 09Public Firebase configuration is not authorization—Security Rules are.

See the architecture working with two devices.

Upload a few slides, create a remote, and scan the QR from your phone.

Open Presentation Viewer