Architecture
How the app is wired together — the stack, the file layout, and how the frontend talks to the backend.
Tech stack
| Layer | Choice | Why |
|---|---|---|
| Framework | TanStack Start v1 + React 19 | File-based routing with SSR-capable server functions on a Cloudflare Worker runtime. |
| Bundler | Vite 7 + @cloudflare/vite-plugin | Fast dev, edge-deployable production build. |
| Styling | Tailwind CSS v4 (native @import in src/styles.css) | Design tokens defined as oklch CSS variables. Dark-first 'Liquid Glass' theme. |
| UI primitives | shadcn/ui (Radix-based) + lucide-react | Accessible building blocks; only what we use is in src/components/ui. |
| Data fetching | @tanstack/react-query | Cached queries, mutations, and invalidation. |
| Backend | Lovable Cloud (Supabase) | Postgres, RLS, Auth, Realtime — accessed via @supabase/supabase-js. |
| Forms | react-hook-form + zod via @hookform/resolvers | Used in onboarding, signup, login. |
| Scraping | Firecrawl (firecrawl-py / HTTP API) | Pulls AMC showtimes for Metreon 16 and Kabuki 8. |
Folder structure
src/
├── routes/ # File-based routing (TanStack)
│ ├── __root.tsx # HTML shell, providers, global meta
│ ├── index.tsx # Landing → redirects into app
│ ├── login.tsx # Email/password + Google sign in
│ ├── signup.tsx
│ ├── invite.tsx # Invite-code gate
│ ├── onboarding.tsx # First-run profile + theater prefs
│ ├── now-playing.tsx # Voting + featured pick
│ ├── upcoming.tsx # Future showtimes by movie
│ ├── movie.$id.tsx # Movie detail + AMC RSVP
│ ├── profile.tsx # Member card, stats, watch log
│ ├── docs.tsx # Docs layout (this section)
│ ├── docs.*.tsx # Docs pages
│ └── api/public/hooks/
│ └── sync-now-playing.ts # AMC scrape endpoint (server route)
├── components/ # Reusable UI
│ ├── ui/ # shadcn primitives
│ ├── AppShell.tsx # Auth gating + tab bar wrapper
│ ├── TopBar.tsx, TabBar.tsx
│ ├── MovieCard.tsx, ShowtimeCard.tsx, AmcShowtimes.tsx
│ ├── VoteButton.tsx, Avatar.tsx
│ └── DocsPage.tsx
├── lib/
│ ├── queries.ts # All React Query hooks + types
│ ├── me.ts # Current user resolver
│ ├── invite-gate.ts # Invite-code validation
│ ├── theaters.ts # Theater catalog
│ ├── format.ts, utils.ts, error-capture.ts, error-page.ts
├── integrations/supabase/ # AUTO-GENERATED — never edit
│ ├── client.ts
│ └── types.ts
├── styles.css # Tailwind v4 + design tokens
└── router.tsx # Router config
supabase/
├── config.toml # Project-level config
└── migrations/ # SQL migrations
Frontend ↔ Backend
The app is a single-page client that talks directly to Supabase via the auto-generated @/integrations/supabase/client. There is no custom API layer — RLS policies are the security boundary.
- Reads: React Query hooks in
src/lib/queries.tswrapsupabase.from(...).select(...). - Writes: Mutations call
supabase.from(...).upsert/insert/deleteand invalidate the matching query keys. - Realtime:
useClubRealtime()subscribes to all 6 public tables and invalidates queries on any change. - Auth:
supabase.authhandles email/password + Google OAuth. Session is read byuseMeId(). - Server route:
/api/public/hooks/sync-now-playingruns in the Worker runtime, scrapes AMC via Firecrawl, and upserts movies + showtimes.
Database schema
| Table | Purpose | Key columns |
|---|---|---|
| club_members | Profile per signed-in user | id, user_id (auth.uid), name, avatar_url, preferred_theaters[], invited, onboarded |
| movies | Currently or recently playing films | id, title, poster_url, genre, runtime_min, slug, currently_playing, added_at |
| amc_showtimes | Scraped AMC screenings | id, movie_id, theater, starts_at, format, ticket_url |
| showtimes | Member-proposed (legacy / manual) plans | id, movie_id, theater, starts_at, proposed_by |
| attendees | RSVPs to a showtime | showtime_id, member_id, seat |
| votes | Heart vote per member per movie | member_id, movie_id |
| watch_logs | Logged watches | id, member_id, movie_id, rating, notes, watched_at, co_attendees[] |
All tables have RLS enabled. movies, amc_showtimes, and showtimes are readable by any authenticated user. Member-owned rows (votes, attendees, watch logs) require auth.uid() = user_id on writes.