Feature spec
IntermediateCategory
vertical-tools
Build with AI
4-8 hours with Lovable
Custom build
1-2 weeks custom dev
Running cost
$0/mo for small events · $50-100/mo at 10K attendees
Works on
Everything it takes to ship a Contactless Check-in System — parts, prompts, and real costs.
A contactless check-in system generates a unique QR code per registration, validates it server-side with idempotent logic (scan twice = still one check-in), and pushes live status to an admin dashboard via Supabase Realtime. With Lovable you can build a working event check-in in 4–8 hours for $0/month on the free tier. The two gotchas that sink first builds: camera access is blocked in preview iframes, and CSV export fails silently without a service_role key in the Edge Function.
What a Contactless Check-in System Actually Is
A contactless check-in system replaces paper sign-in sheets with a QR code workflow: each attendee receives a unique QR code by email after registering; they show it at the venue; a staff member scans it with a phone or a self-service kiosk reads it automatically. The system validates the token, marks the attendee as checked in, and updates the admin dashboard in real time. The three product decisions that define the build are how tokens are generated (UUID per registration is the standard), where validation logic lives (always server-side — client-side validation can be forged), and how the admin dashboard updates (Supabase Realtime is the right answer, not polling). Works for events, clinics, offices, co-working spaces, and hotels.
What users consider table stakes in 2026
- QR code generation per attendee that encodes a unique check-in URL — no app download needed to show or scan
- Guest-facing scan page that works on any mobile browser and shows exactly three states: success (green), already checked in (amber), or invalid token (red)
- Real-time admin dashboard showing checked-in count, pending count, and no-shows that updates as scans happen — no page refresh required
- Kiosk mode that auto-resets to the scan-ready page 3 seconds after a successful or failed scan for the next attendee
- CSV export of the full attendee list with checked-in status, timestamp, and name — downloadable from the admin panel
- Email delivery of the QR code to each attendee after registration with a clear visual of the QR code they need to show at the door
Anatomy of the Feature
Seven components. Lovable handles the QR generation, token store, and real-time dashboard well on the first prompt. The idempotency check in the Edge Function and the CSV export service role key are the two places builds silently break.
QR code generator
Serviceqrcode.react (web) or qr_flutter (Flutter) renders a QR code client-side encoding the full check-in URL: https://app.com/check-in/{token}. The token is a UUID generated when the registration row is created in Supabase — gen_random_uuid() at INSERT time. No API call is needed; QR generation is entirely browser-side.
Note: Always encode the full production URL including the path, not just the token. Tokens generated from window.location.href in a preview environment will encode the preview domain.
Token validation endpoint
BackendA Supabase Edge Function (Deno) that receives a check-in token, checks the registrations table for the token, verifies whether checked_in is already true, and either marks it true with checked_in_at = now() or returns a status indicating the reason. Returns one of three JSON responses: {status: 'success', attendee_name}, {status: 'already_checked_in', checked_in_at}, {status: 'invalid_token'}. The SELECT-before-UPDATE pattern ensures idempotency.
Note: Never run the UPDATE without first checking checked_in — AI tools often generate a simple UPDATE that doesn't return 'already_checked_in' for repeat scans, allowing double check-ins.
Real-time admin dashboard
UIA protected /admin/dashboard page with a Supabase Realtime channel subscription on the registrations table. Shows live counters: Total Registered, Checked In, Pending, No-Show (after event end time). A shadcn/ui Table lists all attendees with name, email, checked-in status, and timestamp. The Realtime subscription uses supabase.channel('check-ins').on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'registrations' }, handleUpdate).subscribe() with an empty dependency array so it stays active for the entire session.
Note: Supabase Realtime must be enabled for the registrations table in Supabase Dashboard → Database → Replication. It is not enabled by default.
Registration and token store
DataSupabase registrations table stores one row per attendee per event. The check_in_token is a UUID generated at INSERT time (gen_random_uuid() default). checked_in is a boolean defaulting to false; checked_in_at is a nullable timestamptz. The token column has a UNIQUE constraint to prevent collisions and to enable fast O(1) lookup.
Note: Index the check_in_token column — the validation Edge Function queries by token on every scan, and a full table scan on large events is unacceptable.
Camera QR scanner (admin scan mode)
Servicehtml5-qrcode library (web) or mobile_scanner Flutter package (mobile) allows staff to scan guest QR codes using the device's camera. The decoded URL is passed to the token validation Edge Function. Camera access requires HTTPS and does not work inside cross-origin iframes — testing must be done on the published app URL.
Note: The html5-qrcode library requires the calling page to be served over HTTPS or localhost. It will silently fail (no error, just no camera) on http:// origins.
Email with QR code
ServiceA Supabase Edge Function triggered after a successful registration INSERT sends the attendee their QR code via Resend. The email uses a React Email template with the QR code rendered as a base64 PNG inline image (generated server-side using the qrcode npm package in Deno). The email includes event name, date, venue, and the QR code image with instructions to 'show this at the door'.
Note: Generate the QR PNG server-side in the Edge Function rather than embedding a URL to a client-rendered QR — email clients often block externally-loaded images.
CSV export
BackendA Supabase Edge Function that reads all registrations for a given event_id and returns them as a CSV file with columns: attendee_name, attendee_email, checked_in (true/false), checked_in_at. Triggered by a button in the admin dashboard. The function must use the Supabase service_role key to bypass RLS, because the event owner's session JWT is not available in the Edge Function context without explicit forwarding.
Note: Using the anon key in the CSV export Edge Function causes it to return zero rows — RLS blocks the query. Always use Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') in admin export functions.
The data model
Two tables: events (optional, for multi-event support) and registrations (the core). Run this in the Supabase SQL editor — it includes the unique token constraint and the index needed for fast scan lookups.
1create table public.events (2 id uuid primary key default gen_random_uuid(),3 name text not null,4 event_date timestamptz,5 location text,6 created_by uuid references auth.users(id) on delete cascade not null,7 created_at timestamptz not null default now()8);910create table public.registrations (11 id uuid primary key default gen_random_uuid(),12 event_id uuid references public.events(id) on delete cascade not null,13 attendee_name text not null,14 attendee_email text not null,15 check_in_token uuid not null unique default gen_random_uuid(),16 checked_in bool not null default false,17 checked_in_at timestamptz,18 created_at timestamptz not null default now()19);2021-- Enable RLS22alter table public.events enable row level security;23alter table public.registrations enable row level security;2425-- Event owners can manage their events26create policy "Event owners can manage events"27 on public.events for all28 using (auth.uid() = created_by)29 with check (auth.uid() = created_by);3031-- Anyone can self-register (INSERT) for an event32create policy "Anyone can register for an event"33 on public.registrations for insert34 with check (true);3536-- Only the event owner can SELECT or UPDATE registrations37create policy "Event owners can view registrations"38 on public.registrations for select39 using (40 exists (41 select 1 from public.events42 where events.id = registrations.event_id43 and events.created_by = auth.uid()44 )45 );4647create policy "Event owners can update check-in status"48 on public.registrations for update49 using (50 exists (51 select 1 from public.events52 where events.id = registrations.event_id53 and events.created_by = auth.uid()54 )55 );5657-- Critical: index for fast token lookup on every scan58create index registrations_token_idx on public.registrations (check_in_token);59create index registrations_event_idx on public.registrations (event_id, checked_in);60create index events_created_by_idx on public.events (created_by);Heads up: The unique constraint on check_in_token prevents token collisions. The index on check_in_token makes every scan validation O(log n) instead of a full table scan. Enable Supabase Realtime for the registrations table in Supabase Dashboard → Database → Replication → select the registrations table — this is not automatic.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Required for NFC wristband or badge scanning, integration with ticketing platforms (Eventbrite, Ticket Tailor), multi-session conference check-in, or feeding check-in data into a CRM in real time.
Step by step
- 1Next.js PWA deployed to Vercel — the check-in scan page works offline using a service worker that queues scans and syncs when connectivity returns
- 2NFC wristband support via the Web NFC API (Chrome on Android) or a hardware NFC reader connected to the kiosk tablet
- 3Eventbrite webhook receiver: Supabase Edge Function listens for order.placed events, inserts registrations, and sends QR code emails automatically
- 4CRM sync: check-in events trigger a Supabase Database Webhook that posts to a Zapier or n8n webhook, creating a 'Checked In' activity in Salesforce or HubSpot
Where this path bites
- NFC via the Web NFC API is Chrome on Android only — iOS does not support Web NFC; iOS NFC requires a native app wrapper
Third-party services you'll need
The core check-in system runs on Supabase and free npm libraries. Costs only appear at scale for email delivery volume.
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Supabase | Database (events + registrations), Edge Functions for token validation and CSV export, Realtime for live dashboard | 500MB DB, 500K Edge Function invocations/mo, Realtime included | $25/mo (Pro) |
| Resend | QR code delivery email to each registrant after sign-up | 3,000 emails/month | $20/mo for 50,000 emails |
| qrcode.react (npm) | Client-side QR code rendering for the attendee ticket page | Free, no API call required | Free |
| html5-qrcode (npm) | Browser camera QR scanning for staff admin scan mode | Free, open-source | Free |
| mobile_scanner (Flutter) | Native QR and barcode scanning for the FlutterFlow kiosk tablet app | Free, open-source (pub.dev) | Free |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
All free tiers sufficient. Supabase free plan covers DB, Realtime, and Edge Functions. Resend free tier covers 3,000 QR code delivery emails. QR generation and scanning libraries are free npm packages.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
QR camera scanner does not activate — blank screen where the video should be
Symptom: The browser Geolocation API and getUserMedia (camera) are blocked in cross-origin iframes unless the iframe explicitly sets allow='camera'. Both Lovable's preview iframe and V0's sandbox iframe block camera access by default. This means the html5-qrcode scanner loads, shows its UI, but the video never starts — no error is thrown, just a black or blank container. Developers often spend an hour debugging a permissions issue that does not actually exist in the real app.
Fix: Do not test camera scanning inside any preview environment. Click Publish in Lovable (or deploy to Vercel from V0) and open the admin scan page on your phone over the published HTTPS URL. Camera access will work correctly on the published app. Add a visible 'Open in new tab' link in the scanner UI to make it easy for staff to leave the preview.
Scanning the same QR code twice checks the attendee in a second time
Symptom: AI tools commonly generate the check-in validation as a simple UPDATE: UPDATE registrations SET checked_in = true WHERE check_in_token = token. Without first checking whether checked_in is already true, this runs successfully on every scan and always returns a success response. A slow-moving crowd with a staff member accidentally scanning the same QR twice creates duplicate check-in records and incorrect counters.
Fix: In the Edge Function, always SELECT first: const { data: reg } = await supabase.from('registrations').select('id, checked_in, checked_in_at, attendee_name').eq('check_in_token', token).single(). If reg.checked_in is true, return {status: 'already_checked_in', checked_in_at: reg.checked_in_at} without running the UPDATE. Only run the UPDATE when checked_in is false.
Real-time dashboard doesn't update when check-ins happen
Symptom: Two separate root causes: (1) Supabase Realtime is not enabled for the registrations table — it requires explicit opt-in in Supabase Dashboard → Database → Replication, and creating the table in Lovable or via SQL does not enable it automatically. (2) The Realtime channel subscription is set up inside a useEffect with dependencies that cause it to be created and immediately cleaned up, so no updates are ever received.
Fix: In Supabase Dashboard, go to Database → Replication and toggle the registrations table to include it in the Realtime publication. In the React component, set up the subscription with an empty dependency array: useEffect(() => { const channel = supabase.channel('check-ins').on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'registrations' }, handleUpdate).subscribe(); return () => { supabase.removeChannel(channel); }; }, []). The empty array ensures the subscription lives for the full component lifetime.
CSV export downloads an empty file
Symptom: The CSV export Edge Function queries the registrations table using the anon Supabase client key. The event-owner-scoped RLS policy (restricting SELECT to the event creator) requires an authenticated JWT to work correctly. An Edge Function called without forwarding the user's auth header runs as the anon role, which the RLS policy blocks — resulting in an empty query result and a CSV with only the header row.
Fix: In the CSV export Edge Function, use the service_role key: const supabase = createClient(Deno.env.get('SUPABASE_URL'), Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')). Store the service_role key in Lovable Secrets or Vercel env vars, never in code. Add a manual authorization check inside the function to verify the requesting user's JWT claims that they own the event before running the query.
Best practices
Always run token validation in a server-side Supabase Edge Function — client-side validation can be bypassed by any attendee who inspects the network request
Implement SELECT-before-UPDATE idempotency in the check-in Edge Function so duplicate scans of the same QR code return 'already_checked_in' instead of counting twice
Enable Supabase Realtime for the registrations table explicitly in the Dashboard — it is not enabled automatically and the admin dashboard will never update without it
Use gen_random_uuid() as the default for check_in_token at INSERT time rather than generating the UUID in application code — database-generated tokens are cryptographically random and never duplicated
Use the SUPABASE_SERVICE_ROLE_KEY in admin-only Edge Functions (CSV export, bulk check-in) and never in client-side code or in functions accessible without authentication
Store the production app URL in an environment variable and read it at QR code render time — never derive it from window.location.href which will encode the preview domain in printed QR codes
Show all three check-in states (success, already_checked_in, invalid_token) with distinct colors and clear copy — 'Already Checked In at 9:47 AM' is more useful to staff than a generic error
Implement kiosk auto-reset with a 3-second timeout after any check-in result so the screen is ready for the next attendee without requiring staff interaction
When You Need Custom Development
Lovable covers single-event QR check-in with a real-time admin dashboard well. These scenarios require a custom build:
- Event requires integration with an existing ticketing platform (Eventbrite, Ticket Tailor, Universe) — their data models and webhook formats need a custom integration layer to sync registrations and adoption status back
- Venue needs NFC wristband or badge tap-to-check-in instead of or alongside QR codes — NFC requires the Web NFC API (Chrome Android only) or a native Swift/Kotlin implementation for cross-platform iOS support
- Multi-session conference where each attendee checks in to individual talks and workshops, generating per-session attendance reports and certificate generation
- Check-in data must feed into a CRM (Salesforce, HubSpot) in real time, triggering contact updates, marketing automation, and post-event follow-up sequences
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
Does the attendee need to download an app to check in?
No. The attendee receives a QR code by email and simply shows it at the venue. The staff-facing scanner runs in the browser on any phone over HTTPS — no app download required on either side. For a fully self-service kiosk, the attendee scans their own code at a tablet running the check-in page in browser kiosk mode. FlutterFlow builds a native iOS/Android kiosk app if you need a more locked-down experience for high-volume events.
How do I prevent someone from sharing their QR code and multiple people checking in with it?
The server-side idempotency check handles this. The second person who scans the same QR code receives the 'Already Checked In at [time]' response — the system does not check them in. For high-security events where you need to catch this in real time, add a photo of the registrant to the success response so staff can visually verify the person matches. The check_in_token is single-use by design: once checked_in = true, no further UPDATE runs on that row.
Can I use the same system for checking in to multiple sessions at a conference?
With the schema as designed, each registration is per-event. For multi-session check-in, extend the schema: add a sessions table (id, event_id, name, start_time), a session_registrations table linking registrations to sessions, and a session_check_ins table recording the per-session scan. Each session gets its own QR code or the same attendee QR code is checked against a session-specific validation endpoint. This extension adds about 4–6 hours to the build.
What happens if a guest's phone battery dies and they can't show the QR code?
Build a manual override in the admin dashboard: a search field that looks up attendees by name or email, and a 'Check In Manually' button that calls the same validation Edge Function with the check_in_token retrieved from the DB. This gives staff a backup that does not bypass the idempotency check — manually checking in an already-checked-in attendee still returns the 'Already Checked In' response.
How do I send check-in QR codes to attendees after they register?
A Supabase Edge Function triggered by the registrations INSERT sends an email via Resend with the QR code as an inline base64 PNG generated server-side using the qrcode npm package (available in Deno). The email template (built with React Email) includes the event name, date, venue, and clear instructions to show the QR at the door. Set up the Edge Function as a Database Webhook trigger on the registrations table for the INSERT event so it fires automatically on every new registration.
Can staff scan QR codes with their phone camera or do they need a special scanner?
Staff use any phone running the /admin/scan page in their browser — html5-qrcode activates the device camera and decodes QR codes in real time without additional hardware. For high-volume events (hundreds of check-ins in minutes), a handheld Bluetooth HID barcode scanner reads faster and more reliably than a phone camera in bright sunlight. Bluetooth HID scanners work with the web app out of the box — they send keystrokes to the active input field, and the check-in page can listen for that input pattern.
How do I export the check-in data for compliance or reporting?
The CSV export button in the admin dashboard calls a Supabase Edge Function that reads all registrations for the event using the service_role key (to bypass RLS), formats them as CSV with columns for name, email, checked_in status, and check-in timestamp, and returns the response with Content-Type: text/csv and Content-Disposition: attachment headers. The browser triggers a file download automatically. The same function can be extended to return XLSX format using a Deno-compatible spreadsheet library if your event clients need Excel format.
Can I set up a self-service kiosk at the venue entrance?
Yes. The check-in page at /check-in/[token] is designed to be the kiosk screen. Point a tablet at the check-in page in browser kiosk mode (Full Screen + Home button disabled). Attendees scan their own QR code using the admin scanner page, or the kiosk displays an html5-qrcode reader fullscreen. After 3 seconds on the success or error screen, the page auto-resets. For a fully locked-down kiosk, use a dedicated Android tablet with a kiosk browser app (Fully Kiosk Browser) that prevents attendees from leaving the check-in page.
Need this feature production-ready?
RapidDev builds a contactless check-in system into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.