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)

HookReturnsUnderlying query
useMembers()Member[]club_members.select('*').order('name')
useMovies()Movie[]movies.select('*').eq('currently_playing', true).order('added_at')
useMovie(id)Movie | nullmovies.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()voidSubscribes to all 6 public tables and invalidates matching React Query keys on any change.
useMeId()string | null | undefinedReads 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:

ActionWhereCall
Toggle voteVoteButton.tsxupsert / delete on votes, invalidate ['votes']
RSVP to AMC showtimeAmcShowtimes.tsxupsert / delete on attendees, invalidate ['attendees']
Log a watchprofile.tsxinsert into watch_logs, invalidate ['watch_logs']
Save profileonboarding.tsx, profile.tsxupdate club_members, invalidate ['members']
Validate invite codeinvite.tsx → invite-gate.tschecks 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-playing

Security 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,
});