API
There is no custom REST layer — the app reads and writes Supabase directly. This page documents the React Query hooks that wrap those calls and the one server route used for scraping.
React Query hooks (src/lib/queries.ts)
| Hook | Returns | Underlying query |
|---|---|---|
useMembers() | Member[] | club_members.select('*').order('name') |
useMovies() | Movie[] | movies.select('*').eq('currently_playing', true).order('added_at') |
useMovie(id) | Movie | null | movies.select('*').eq('id', id).maybeSingle() |
useVotes() | Vote[] | votes.select('*') |
useShowtimes() | Showtime[] | showtimes.select('*').order('starts_at') |
useAttendees() | Attendee[] | attendees.select('*') |
useWatchLogs() | WatchLog[] | watch_logs.select('*').order('watched_at', { ascending: false }) |
useAmcShowtimes(movieId) | AmcShowtime[] | amc_showtimes.select('*').eq('movie_id', movieId).gte('starts_at', now).order('starts_at') |
useClubRealtime() | void | Subscribes to all 6 public tables and invalidates matching React Query keys on any change. |
useMeId() | string | null | undefined | Reads supabase.auth session and resolves club_members.id for the current user. |
Mutations
Mutations live alongside the components that trigger them. Each one calls the Supabase SDK directly and invalidates the relevant query key. Examples:
| Action | Where | Call |
|---|---|---|
| Toggle vote | VoteButton.tsx | upsert / delete on votes, invalidate ['votes'] |
| RSVP to AMC showtime | AmcShowtimes.tsx | upsert / delete on attendees, invalidate ['attendees'] |
| Log a watch | profile.tsx | insert into watch_logs, invalidate ['watch_logs'] |
| Save profile | onboarding.tsx, profile.tsx | update club_members, invalidate ['members'] |
| Validate invite code | invite.tsx → invite-gate.ts | checks code, sets club_members.invited = true |
Server route
POST /api/public/hooks/sync-now-playing
Refreshes movies and AMC showtimes by scraping amctheatres.com via Firecrawl. Iterates two theaters (Metreon 16, Kabuki 8) over 21 days, upserts movies, replaces future showtimes, marks movies that disappeared as currently_playing: false (preserves votes / watch logs / attendees).
Inputs: none.
Output: JSON summary { movies: n, showtimes: n, deactivated: n }.
Required secret: FIRECRAWL_API_KEY in the Lovable Cloud secrets.
curl -X POST https://sfmovies.lovable.app/api/public/hooks/sync-now-playingSecurity note: the endpoint is currently unauthenticated. Anyone can trigger it, which burns Firecrawl credits. Consider adding a shared-secret header check before exposing it more widely.
Direct Supabase writes you'll see in code
// Upsert a vote
await supabase.from('votes').upsert({ member_id, movie_id });
// Remove a vote
await supabase.from('votes').delete()
.eq('member_id', member_id).eq('movie_id', movie_id);
// RSVP to an AMC showtime
await supabase.from('attendees').upsert({
showtime_id, member_id, seat: null,
});
// Log a watch
await supabase.from('watch_logs').insert({
member_id, movie_id, rating, notes, watched_at, co_attendees,
});