A gamified event app built with Next.js 14 (App Router) + TypeScript (strict) + Supabase. Players create an avatar, visit event stands, complete activities, and collect pieces/badges. Staff members validate player activities at their assigned stand.
- Next.js 14 (App Router, file-based routing)
- TypeScript strict mode
- Supabase (Auth + Postgres) for identity and persistence
- Avatars rendered as code-generated SVG sprites (no image assets, no uploads)
- Node.js 18+
- npm (the repo uses
package-lock.json; ignore any straybun.lock) - A Supabase project
The browser app reads exactly two environment variables (see
src/infrastructure/supabase-client.ts). Create a .env.local file in the project root:
NEXT_PUBLIC_SUPABASE_URL=https://YOUR-PROJECT-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR-anon-public-key
# Server-side only (NOT public, never bundled into the client). Used by the
# integration tests and the `npm run seed:admin` tooling, not by the app runtime.
SUPABASE_SERVICE_ROLE_KEY=YOUR-service-role-key
ADMIN_EMAILS=admin@example.com,other-admin@example.com
# Optional · reserved for future storage. NOT read by any code yet — see "Storage buckets".
# NEXT_PUBLIC_SUPABASE_BUCKET=avatars| Variable | Required | Where to find it |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Yes | Supabase Dashboard → Project Settings → API → Project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Yes | Supabase Dashboard → Project Settings → API → Project API keys → anon public |
SUPABASE_SERVICE_ROLE_KEY |
Server-side | Supabase Dashboard → Project Settings → API → Project API keys → service_role secret. Never expose this in the browser. Used only by integration tests and the admin allowlist seed. |
ADMIN_EMAILS |
Server-side | Comma-separated list of emails that should become admins. Seeded into the admin_allowlist table via npm run seed:admin. Not public, not read by the app runtime. |
NEXT_PUBLIC_SUPABASE_BUCKET |
No | Reserved placeholder for a future Supabase Storage bucket. No code reads it today; setting it has no effect until storage is implemented. |
The two NEXT_PUBLIC_* variables are used by the browser client. The anon key is safe
to expose; row-level security and column grants enforce access control server-side. The
SUPABASE_SERVICE_ROLE_KEY and ADMIN_EMAILS variables are server-side only — they are
consumed by tests and tooling, never shipped to the browser.
If these are unset, supabaseConfigured() returns false and the app falls back to
localStorage-only mode (no cross-device persistence, no auth).
Set the same two variables in your Vercel project (Settings → Environment Variables) for deployed environments.
None. This app does not use Supabase Storage.
- Avatars are generated in code (
src/presentation/components/sprites.tsx), not stored files. - All player state lives in the
profilestable (theprogressjsonb column), not in object storage. - There are no
upload,getPublicUrl, orcreateBucketcalls anywhere in the codebase.
You do not need to create a bucket for the app to work today.
The optional NEXT_PUBLIC_SUPABASE_BUCKET variable above is a reserved placeholder only: if
file uploads (e.g. real avatar images) are added later, the bucket name will be read from .env
instead of hardcoded. Until that feature exists, the variable is unused and setting it does nothing.
- Create a Supabase project.
- Open SQL Editor, paste the contents of
supabase/schema.sql, and run it. This creates:- the
profilestable (1:1 withauth.users) with RLS (owner-only access); - column-level grants — clients may only write
progress,base_id,updated_at; become_staff(p_stand_id, p_access_code)andchange_stand(p_stand_id)security-definer RPCs;- a signup trigger that auto-creates a profile from auth metadata.
- the
- Disable email confirmation: Authentication → Providers → Email → turn off "Confirm email" (the signup flow logs the user in immediately).
- Set the staff access code: edit the hardcoded
'4242'in thebecome_stafffunction insupabase/schema.sqlbefore each event, then re-run that function block. - Copy the Project URL and
anonkey into.env.local(see above).
- A player becomes staff by calling the
become_staffRPC with the event access code and a valid stand id (cloud,ia,sec,crew,build). roleandstand_idare write-locked for clients (revoked column grants); they can only change through the security-definer RPCs. Direct column writes are rejected.
Admins are not promoted from inside the app — there is no client path to write role. Instead,
the role is granted at signup time from a server-side allowlist:
- Put the admin's email in
ADMIN_EMAILS(comma-separated, server-side, non-public). - Run
npm run seed:adminto upsert those emails into theadmin_allowlisttable (service-role only — clients cannot read or write it). The command is idempotent; re-run it wheneverADMIN_EMAILSchanges. - The allowlisted person signs up normally with that email. The
handle_new_usersignup trigger sees the email inadmin_allowlistand creates the profile withrole = 'admin'. - The app recognizes the admin role and exposes a guarded
/adminroute. The full admin console is SP2; today the route only renders a placeholder for admins and redirects everyone else.
Multi-event note: the schema has moved beyond the original single-event model. Identity now lives in
profiles, per-event progress inparticipations, and the catalog (events, stands, activities, badges, prizes) is event-scoped. Seesupabase/migrations/for the current schema; the legacy single-eventsupabase/schema.sqlis retained for reference.
npm install
npm run dev # http://localhost:3000
npm run build # production build
npm run start # serve the production build
npm test # run the integration test suite (Vitest, real Supabase)
npm run seed:admin # upsert ADMIN_EMAILS into the admin_allowlist tableIf npm run build fails with ENOENT .next/server/pages/_app.js.nft.json, remove the build
cache and rebuild:
rm -rf .next && npm run buildapp/ Next.js routes (login, register, staff, scanner, ...)
src/
domain/ types, catalog (stands, activities, stand access codes)
application/ use cases + ports (approve-activity, ...)
infrastructure/ supabase-client, supabase-game-repository, local-storage-...
presentation/
state/game-provider.tsx auth lifecycle, debounced write-behind saves, legacy migration
screens/ components/ UI + code-generated avatar sprites
supabase/schema.sql DB schema, RLS, column grants, RPCs, signup trigger