Two-day event check-in app. Customers scan a printed QR code, type their email or phone, and get checked in. New attendees fill out a short form. All records live in a Supabase Postgres database.
attendance/
├── shared/ # event dates + day detection (used by both client and server)
├── sql/ # database setup + post-import normalization
├── server/ # Node + Express API
├── client/ # React + Vite frontend
├── qr/ # printable QR poster generator
├── LUMA_IMPORT.md # how to import Luma CSV into Supabase
└── README.md
Follow these steps in order.
Edit shared/eventConfig.js:
EVENT_NAME— appears in the UI and on the printed posterDAY1_DATEandDAY2_DATE—YYYY-MM-DDformat, used to auto-detect which day to check guests in forTIMEZONE_OFFSET_HOURS— hours offset from UTC (Nigeria is+1)
- Create a new Supabase project.
- Go to SQL Editor → paste the contents of sql/setup.sql → run.
- Project Settings → API → copy:
Project URL→SUPABASE_URLservice_rolesecret →SUPABASE_SERVICE_KEY
Follow LUMA_IMPORT.md. Summary:
- Export approved guests from Luma as CSV.
- Trim columns to
full_name, email, phone, source. - Import via Supabase Table Editor.
- Run sql/post_luma_import.sql to normalize emails and phones.
cd server
cp .env.example .envEdit server/.env:
SUPABASE_URLandSUPABASE_SERVICE_KEYfrom step 2EVENT_TOKEN— invent a random string (e.g.openssl rand -hex 16)CLIENT_ORIGIN— leave ashttp://localhost:5173for local dev
Install + run:
npm install
npm run devThe server listens on http://localhost:4000. Test with curl http://localhost:4000/api/health.
cd client
cp .env.example .envEdit client/.env:
VITE_API_BASE_URL—http://localhost:4000for local dev, your deployed server URL in productionVITE_EVENT_TOKEN— must match the server'sEVENT_TOKEN
Install + run:
npm install
npm run devOpen http://localhost:5173/checkin.
cd qr
npm install
node generate.js "https://your-deployed-checkin-url.com/checkin"This writes event-qr.png. Then open qr/print.html in a browser, optionally edit the title, and File → Print to A4. The QR encodes whatever URL you passed to generate.js.
Tip: print one QR per day, with
?day=1and?day=2in the URL, to override the auto-detection. Example:https://…/checkin?day=1
- Customer scans QR → opens
/checkinon their phone. - They type email or phone →
POST /api/lookup. - Found: confirm card asks "Is this you?" → on Yes,
POST /api/checkinupdatesday1_checkinorday2_checkin(only if currentlyNULL, so re-scans don't overwrite). - Not found: registration form for full name, email, phone, address →
POST /api/registerinserts the record and marks today's day as checked in. - Success screen for 8 seconds, then resets to the lookup screen.
- The client computes
dayfromeventConfig.jsand sends it on every request. - A
?day=1or?day=2URL query string overrides auto-detection — useful if you want a single QR per day, or if you need to manually correct on-site.
- Re-scanning never overwrites a check-in timestamp. The API returns
alreadyCheckedIn: trueand the UI shows "You're already in". - Duplicate registration attempts (same email or phone already in
attendees) return a 409 with a friendly message.
- Every successful check-in writes a row to the
check_instable withattendee_id,day,checked_in_at, andsource_ip. - Useful for "who checked in at 9:14am" or "did anyone double-scan" after the event.
- Write endpoints require an
x-event-tokenheader that matchesEVENT_TOKENon the server. - The token is shipped in the client bundle, so this is anti-casual-abuse, not real auth.
service_roleSupabase key is server-side only. Never expose it to the client.
- The server is a plain Node/Express app — deploy to Railway, Render, Fly, or anywhere that runs Node.
- The client is a static SPA (
npm run buildoutputsdist/) — deploy to Vercel, Netlify, Cloudflare Pages, or the same host as the server. - After deploying the client, regenerate the QR with the production URL.
- Update
CLIENT_ORIGINon the server to match the deployed client origin.
| Problem | Likely cause |
|---|---|
unauthorized on every request |
VITE_EVENT_TOKEN (client) doesn't match EVENT_TOKEN (server). |
| Lookup says "not found" for someone you imported | email_normalized / phone_normalized weren't populated — re-run sql/post_luma_import.sql. |
| "Today is not an event day" warning | Today's date doesn't match DAY1_DATE or DAY2_DATE in eventConfig.js. Either fix the dates or pass ?day=1 / ?day=2 in the URL. |
| Phone lookup fails | The customer's phone format must reduce to the same last-10-digits as the imported value. The normalizer handles +234, leading 0, spaces, dashes, and parentheses — anything else, look at the raw value in Supabase. |