CLUTCH SQUARESDocs · v1
Developer reference

Integration & API Documentation

How Clutch Squares delivers realtime game state to every connected client and how NBA live scores flow into the board.

01 — Overview

Architecture at a glance

Clutch Squares runs on Lovable Cloud, which provides a Postgres database, row-level security, and a realtime channel layer. Every connected player, host, and TV overlay subscribes to the same game-scoped channel and receives row-level changes as they happen.

  • games — the source of truth for score, clock, quarter, and status.
  • squares — 100 cells per game, owned by players.
  • game_players — roster of who is in the lobby.
  • messages — chat stream during the game.
02 — Realtime

Subscribing to game state

Realtime updates use Postgres change data capture exposed over a websocket channel. A single channel per game streams inserts, updates, and deletes for the four tables above.

The reference implementation lives in src/hooks/useGame.ts. Subscribe with the client SDK like this:

import { supabase } from "@/integrations/supabase/client";

const channel = supabase
  .channel(`game:${gameId}`)
  .on(
    "postgres_changes",
    { event: "*", schema: "public", table: "games", filter: `id=eq.${gameId}` },
    (payload) => {
      // payload.new is the updated game row
      // payload.eventType is "INSERT" | "UPDATE" | "DELETE"
    },
  )
  .on(
    "postgres_changes",
    { event: "*", schema: "public", table: "squares", filter: `game_id=eq.${gameId}` },
    (payload) => { /* a square was claimed or released */ },
  )
  .subscribe();

// Always clean up on unmount
return () => { supabase.removeChannel(channel); };

Payloads follow the standard Postgres CDC format: eventType, new,old, and schema. Treat them as authoritative — do not optimistically mutate state without reconciling against the broadcast.

Anonymous subscribers (e.g. the public TV overlay) cannot use realtime because RLS gates the channel. For those views, poll get_overlay_by_token on a 3-second interval instead — see src/routes/overlay.$token.tsx.

03 — Live scores

NBA score ingestion

Scores currently flow into the board through the host's scoring console rather than an upstream NBA feed. The host edits a draft in score_drafts and commits it to the games row, which broadcasts to every client and triggers winner detection.

Commit a score update from the host UI:

await supabase
  .from("games")
  .update({
    home_score: 88,
    away_score: 85,
    quarter: 4,
    clock: "2:14",
  })
  .eq("id", gameId);

Validation rules (enforced client-side in the scoring console): scores must be non-negative integers, quarter is 1–4 or overtime, and clock matches M:SS or MM:SS.

Plugging in a live NBA provider. To replace manual entry with an upstream feed (e.g. a sports data API), call the same update from a server function on a poll or webhook. The realtime layer downstream does not change:

// Pseudocode for a future server-side ingest
const live = await fetch("https://your-nba-provider/games/" + externalId).then(r => r.json());

await supabaseAdmin
  .from("games")
  .update({
    home_score: live.homeScore,
    away_score: live.awayScore,
    quarter: live.period,
    clock: live.clock,
    status: live.isFinal ? "completed" : "live",
  })
  .eq("id", gameId);

Winner calculation is deterministic and runs on the client from the committed score — see winningSquareIndex in src/lib/types.ts.

04 — Schema

Key tables

The shape of each broadcast payload mirrors these tables:

games (
  id uuid primary key,
  status        game_status,        -- lobby | locked | live | completed
  home_score    int,
  away_score    int,
  quarter       int,
  clock         text,               -- "M:SS"
  home_axis     int[],              -- shuffled 0..9
  away_axis     int[],              -- shuffled 0..9
  share_token   text                -- public overlay token
)

squares (
  id uuid primary key,
  game_id uuid references games(id),
  row int, col int,                 -- 0..9
  owner_id uuid, owner_name text
)
05 — Auth

Access rules

All write paths are protected by row-level security. A user can update games only if they are the host (is_game_host), and can claim a square only if can_claim_square returns true. Reads are scoped to game members via is_game_member.

For the public watch-party overlay, the host shares a URL containing games.share_token. The get_overlay_by_token RPC is the only path that returns game data without an authenticated session, and it exposes a read-only snapshot.