A two-part site for ETCODE 4, a competitive programming contest by ENSIA Tech Community. Landing page + a three-path registration flow (Create Team / Join Team / Solo) backed by Supabase.
Next.js 16 (App Router) · React 19 · TypeScript · Tailwind v4 · Supabase · Motion · Lenis.
npm install
npm run dev # http://localhost:3000npm run build / npm run start for production, npm run lint for eslint.
The forms validate without keys, but writes need Supabase:
-
Create a project at supabase.com.
-
Open the SQL editor and run
supabase/schema.sql. It creates theteamsandparticipantstables, the atomicjoin_teamRPC (enforces the 3-player cap), and locks the tables behind RLS. -
In Project settings → API, copy the values into
.env.local:NEXT_PUBLIC_SUPABASE_URL=https://<project>.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon public key> SUPABASE_SERVICE_ROLE_KEY=<service role secret>The service-role key is server-only — it's used solely by Server Actions (
lib/actions.ts) and never reaches the browser. Never prefix it withNEXT_PUBLIC_. -
Restart
npm run dev.
- SQL injection — all DB access goes through the Supabase client (parameterized
queries) and the
join_teamRPC (parameterizedjsonb). No string-built SQL. - CSRF — registration writes are Next.js Server Actions, which are origin-checked by the framework; no custom endpoints are exposed.
- Service-role isolation — the service-role key is
server-onlyand used solely inlib/actions.ts. RLS denies the anon key direct table access. - Validation twice — zod on the client (RHF resolver) and again in every action. Inputs are trimmed and length-capped.
- Rate limiting — per-IP sliding window in
lib/rate-limit.ts(create/join/ solo/verify). In-memory + best-effort; back it with Upstash Redis for multi-instance production. - Bot trap — a hidden honeypot field; submissions that fill it are silently dropped.
All copy and data live in lib/content.ts (event facts, stats, format,
agenda, mentors, FAQ, prizes, sponsors). When the real date/venue/lineup arrive,
only this file changes. Registration fields live in lib/schema.ts — change a
field there and in supabase/schema.sql together.