Data flow
Tracing data from user input through the UI, into Supabase, and back out to every connected client via realtime.
High-level diagram
┌────────────┐ React Query ┌───────────────┐
│ Route │ ─────hooks────────▶│ supabase.from │
│ Component │◀───cached data─────│ (Postgres) │
└─────┬──────┘ └───────┬───────┘
│ user action │
▼ │
┌────────────┐ .insert/.upsert │
│ Mutation │ ──────────────────────────▶│
│ (in comp.) │ │
└─────┬──────┘ │
│ qc.invalidateQueries() │
▼ │
refetch │
▼
┌──────────────────┐
│ Realtime channel │
│ (all 6 tables) │
└────────┬─────────┘
│
invalidates queries on every other client
Authentication flow
- User lands on
/loginor/signup. Email/password or Google OAuth viasupabase.auth. - On success,
useMeId()reads the session and resolvesclub_members.idfor the currentauth.uid(). AppShellchecks the member row:- No
invitedflag → redirect to/invite(validates codeabsolutecinemaserver-side viainvite-gate.ts, setsinvited = true). invitedbut notonboarded→ redirect to/onboarding(collects name + preferred theaters, setsonboarded = true).- Otherwise → render the requested route.
- No
- On every page,
useClubRealtime()subscribes to all 6 public tables. Any insert/update/delete invalidates the relevant React Query keys so all clients stay in sync.
Read path (example: Now Playing)
now-playing.tsx
├─ useMovies() → SELECT * FROM movies WHERE currently_playing
├─ useVotes() → SELECT * FROM votes
└─ useMembers() → SELECT * FROM club_members
↓
Sort movies by vote count → render <MovieCard /> grid
Write path (example: voting)
User taps <VoteButton />
↓
voted ? supabase.from('votes').delete().match({member_id, movie_id})
: supabase.from('votes').upsert({member_id, movie_id})
↓
qc.invalidateQueries({ queryKey: ['votes'] })
↓
useVotes() refetches → MovieCard re-renders
↓
Realtime fires INSERT/DELETE on votes
↓
Other connected clients invalidate ['votes'] and refetch
Showtime sync path
POST /api/public/hooks/sync-now-playing
↓
For each AMC theater (Metreon 16, Kabuki 8):
Firecrawl scrape next 21 days
↓
Upsert movies (currently_playing: true)
↓
Mark stale movies currently_playing: false (preserves votes/logs)
↓
Replace future amc_showtimes for tracked theaters
↓
Realtime fires → all clients refetch
Failure modes
- RLS denial: any read returning empty when data exists usually means the policy doesn't grant the current role. Check policies on the affected table.
- Stale UI: a mutation that doesn't invalidate the right query key. Realtime will catch it within a second, but the originating client should also invalidate.
- Sync silently empty: Firecrawl key missing or AMC HTML changed — check the server-route logs.