Skip to main content
RapidDev - Software Development Agency
App Featuresvertical-tools23 min read

How to Add a Contactless Check-in System to Your App (Copy-Paste Prompts Included)

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.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

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

WebMobile

Everything it takes to ship a Contactless Check-in System — parts, prompts, and real costs.

TL;DR

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.

Layers:UIDataBackendService

QR code generator

Service

qrcode.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

Backend

A 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

UI

A 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

Data

Supabase 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)

Service

html5-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

Service

A 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

Backend

A 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.

schema.sql
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);
9
10create 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);
20
21-- Enable RLS
22alter table public.events enable row level security;
23alter table public.registrations enable row level security;
24
25-- Event owners can manage their events
26create policy "Event owners can manage events"
27 on public.events for all
28 using (auth.uid() = created_by)
29 with check (auth.uid() = created_by);
30
31-- Anyone can self-register (INSERT) for an event
32create policy "Anyone can register for an event"
33 on public.registrations for insert
34 with check (true);
35
36-- Only the event owner can SELECT or UPDATE registrations
37create policy "Event owners can view registrations"
38 on public.registrations for select
39 using (
40 exists (
41 select 1 from public.events
42 where events.id = registrations.event_id
43 and events.created_by = auth.uid()
44 )
45 );
46
47create policy "Event owners can update check-in status"
48 on public.registrations for update
49 using (
50 exists (
51 select 1 from public.events
52 where events.id = registrations.event_id
53 and events.created_by = auth.uid()
54 )
55 );
56
57-- Critical: index for fast token lookup on every scan
58create 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.

Hand-built by developersFit for this feature:

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

  1. 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
  2. 2NFC wristband support via the Web NFC API (Chrome on Android) or a hardware NFC reader connected to the kiosk tablet
  3. 3Eventbrite webhook receiver: Supabase Edge Function listens for order.placed events, inserts registrations, and sends QR code emails automatically
  4. 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.

ServiceWhat it doesFree tierPaid from
SupabaseDatabase (events + registrations), Edge Functions for token validation and CSV export, Realtime for live dashboard500MB DB, 500K Edge Function invocations/mo, Realtime included$25/mo (Pro)
ResendQR code delivery email to each registrant after sign-up3,000 emails/month$20/mo for 50,000 emails
qrcode.react (npm)Client-side QR code rendering for the attendee ticket pageFree, no API call requiredFree
html5-qrcode (npm)Browser camera QR scanning for staff admin scan modeFree, open-sourceFree
mobile_scanner (Flutter)Native QR and barcode scanning for the FlutterFlow kiosk tablet appFree, 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

$0/mo

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

1

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

2

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

3

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

4

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

5

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

6

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

7

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

8

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.

Get a fixed-price quote

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.

RapidDev

Need this feature production-ready?

RapidDev builds a contactless check-in system into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.