Integration & API Documentation
How Clutch Squares delivers realtime game state to every connected client and how NBA live scores flow into the board.
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.
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.
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.
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
)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.