Skip to content

Latest commit

 

History

History
77 lines (58 loc) · 5.98 KB

File metadata and controls

77 lines (58 loc) · 5.98 KB

When Works

Next.js scheduling app — users create events, share a link, collect availability without sign-ups. Two creator access methods (Google sign-in, private owner link) plus an admin dashboard.

Stack

  • Next.js 16.2 (App Router)
  • Supabase (Postgres + RLS)
  • next-auth 4.24 (Google OAuth)
  • React 19, vanilla CSS (no Tailwind build — colors hardcoded from Tailwind palette)

Commands

npm run dev      # :3000
npm run build
npm run lint     # ESLint, next/core-web-vitals

No test suite configured.

Conventions that will burn you if missed

  • Dates: stored as YYYY-MM-DD ISO strings. Always parse as new Date(dateString + 'T12:00:00') — noon UTC to dodge timezone bugs.
  • Styling: no Tailwind build. Use hex from app/globals.css (slate-950 #0f172a, slate-800 #1e293b, indigo #6366f1, emerald #10b981). Don't reach for Tailwind classes.
  • Admin password is ADMIN_PASSWORD — server-only, checked per-request via the x-admin-password header (lib/adminAuth.js). Never expose it in a NEXT_PUBLIC_ var.
  • API routes require dynamic = 'force-dynamic' and runtime = 'nodejs' for NextAuth.
  • The browser never talks to Supabase. All reads/writes go through app/api/ routes using the service-role client; RLS denies the anon role on every table. Never add @supabase/supabase-js usage to client components.
  • Capability tokens are the guest auth model: manage_token (owner — events AND groups), participant_token (device-wide identity, global localStorage key when_works_participant_token), member_token (group member's personal invite link, ?m= on /respond/[slug] — accepted in request bodies, never echoed back), response_token (legacy per-event respondent token — still accepted in request bodies, never returned or written client-side anymore), invite_token (hosting follow-up). Tokens are minted server-side and stored in the visitor's localStorage.
  • Identity = participants table (lib/participants.js): signed-in users resolve by normalized email, guests by participant_token. The legacy identity columns (google_email/owner_user_id/owner_email) and user_profiles were dropped by 005_cleanup_legacy_identity.sql — never reference them. Emails are ALWAYS normalized (normalizeEmail) before storage or lookup.
  • Responses soft-delete via deleted_at — every responses query must filter .is('deleted_at', null) unless it deliberately wants deleted rows (guest numbering, owner restore list, export).
  • Schedule/cron date math lives in lib/schedule.js (pure, dependency-free, client-importable — "today" is always a parameter). Group cadence is hybrid: cadence_unit 'day'|'month' + cadence_interval + cadence_anchor_day; never reference the deprecated cadence_days.
  • All cron mutations use compare-and-set updates (update … where <marker still unclaimed> + .select(), 0 rows = another invocation won). The daily cron (app/api/cron/daily, registered in vercel.json) auto-creates group polls, sends pre-send notices, sends deadline summaries, and auto-schedules opted-in polls; events.owner_summary_sent_at is the single owner-summary marker shared with the respond route's all-responded hook — always claim before sending.
  • Auto-scheduling (opt-in per group schedule): summaries stamp events.auto_schedule_on (generation = next cron run ≥1 day later); the cron CAS-claims events.auto_scheduled_at before calling Google, then records a CLOSED event_followups round (lib/hostingRounds.js). The owner's Google refresh token lives in participants.google_refresh_token (server-only, captured at sign-in in lib/auth.js). Pick/bucket/validation logic is pure in lib/autoSchedule.js; Google calls in lib/googleCalendar.js. See docs/decisions.md.

Where things live

  • app/respond/[slug]/ — public response flow
  • app/events/manage/[token]/ — creator management
  • app/groups/ + app/groups/manage/[ref]/ — groups (roster, cadence nudge, attendance scores, automatic-poll schedule)
  • app/groups/pause/[token]/ — one-click pause landing page (emailed capability)
  • app/admin/ — password-gated admin
  • app/api/ — server routes
  • app/api/cron/daily/ — daily Vercel Cron (auto-create polls, pre-send notices, deadline summaries); auth = Authorization: Bearer CRON_SECRET or x-admin-password (admin may pass ?date= for testing)

Deeper docs (load on demand)

  • docs/schema.md — full Supabase table reference
  • docs/access-control.md — three-path ownership resolution + RLS policies
  • docs/response-flow.md — debounced auto-save, confirmation lock, snapshot/reset
  • docs/followups.md — hosting round flow, timezone normalization

Environment Variables

Required for deployment:

NEXT_PUBLIC_SUPABASE_URL
SUPABASE_SERVICE_ROLE_KEY         # Server-only; the ONLY Supabase key in use
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET
ADMIN_PASSWORD                    # Server-only, admin dashboard password
CRON_SECRET                       # Server-only; Vercel sends it as the Bearer token on cron invocations
NEXTAUTH_SECRET                   # For JWT signing (auto-generated by next-auth)
NEXTAUTH_URL                      # Callback URL (prod: full domain, dev: http://localhost:3000)

Optional (group email notifications no-op gracefully without them):

RESEND_API_KEY                    # Server-only, Resend transactional email
RESEND_FROM_EMAIL                 # e.g. "When Works <notify@yourdomain>"

SQL migrations live in supabase/migrations/ and are run manually in the Supabase SQL editor (see file headers for ordering constraints).

Two Supabase projects since July 2026: dev (local) and staging share one Supabase project; production uses a separate one. Test data never touches prod, but every migration must be run in BOTH projects — dev/staging first, prod when the code deploys there. Within each environment, additive migrations still run before the code that needs them deploys, and destructive drops still wait until the referencing code is gone.