Skip to main content

Overview

PassTru’s QR code check-in system enables fast, contactless attendee verification using automatically generated QR codes and browser-based scanning.

How It Works

1

QR Code Generation

Each attendee receives a unique QR code containing their portal URL when added to an event.
2

Attendee Receipt

QR codes are delivered via confirmation email or accessible through the attendee portal.
3

Check-In Scanning

Staff or attendees scan QR codes using the public check-in page’s camera scanner.
4

Instant Verification

System validates the code, marks attendance, and activates the attendee portal.

QR Code Generation

QR codes are generated client-side using the react-qr-code library:
src/pages/public/AttendeePortal.tsx
import QRCode from "react-qr-code";

<QRCode
  value={portalUrl}
  size={200}
  bgColor="#ffffff"
  fgColor="#000000"
  level="M"
/>

QR Code Content

Each QR code encodes the attendee’s unique portal URL:
https://yourdomain.com/{org-slug}/{event-slug}/attendee/{unique-id}
Example QR Code URL:
https://passtru.com/acme-events/annual-gala-2026/attendee/AB12CD34

Scanning Methods

PassTru supports two check-in methods:

Camera-Based Scanning

Uses the device camera to scan QR codes:
  • Opens device camera (requires permission)
  • Real-time QR detection
  • Automatic parsing and validation
  • Instant check-in feedback
Implementation:
src/components/QrScanner.tsx
const scanner = new Html5Qrcode(scannerId);

scanner.start(
  { facingMode: "environment" },
  { fps: 10, qrbox: { width: 250, height: 250 } },
  (decodedText) => {
    onScan(decodedText);
    scanner.stop();
  }
);

Check-In Flow

1. Access Check-In Page

Navigate to the public check-in URL:
/{org-slug}/{event-slug}/welcome

2. Select Check-In Method

Two prominent buttons on the check-in page:
src/pages/public/PublicCheckIn.tsx
<Button onClick={() => setMode("qr")}>
  <QrCode className="mr-3 h-6 w-6" />
  Scan My QR Code
</Button>

<Button variant="outline" onClick={() => setMode("manual")}>
  <Keyboard className="mr-3 h-6 w-6" />
  Manual Check-In
</Button>

3. Perform Check-In

The check-in process validates attendee identity and updates their status:
src/pages/public/PublicCheckIn.tsx
const performCheckIn = async (attendeeUid: string, attendeeEmail?: string) => {
  const body = {
    event_id: event.id,
    unique_id: attendeeUid.trim(),
    method: attendeeEmail ? "self_service" : "qr_scan",
    ...(attendeeEmail && { email: attendeeEmail.trim() })
  };
  
  const { data, error } = await supabase.functions.invoke(
    "public-checkin",
    { body }
  );
  
  // Handle response statuses
};

4. Check-In Responses

{
  "status": "success",
  "attendee_name": "John Doe",
  "attendee_unique_id": "AB12CD34"
}
  • Mark attendee as checked in
  • Set check-in timestamp
  • Activate attendee portal
  • Display success message

QR Scanner Component

The scanner uses html5-qrcode for browser-based QR detection:
src/components/QrScanner.tsx
import { Html5Qrcode } from "html5-qrcode";

export function QrScanner({ onScan, onError, scanning, onStop }) {
  const scannerRef = useRef<Html5Qrcode | null>(null);
  
  useEffect(() => {
    if (!scanning) return;
    
    const scanner = new Html5Qrcode("qr-scanner-region");
    scannerRef.current = scanner;
    
    scanner.start(
      { facingMode: "environment" },  // Use rear camera
      { fps: 10, qrbox: { width: 250, height: 250 } },
      (decodedText) => {
        onScan(decodedText);
        scanner.stop();
      }
    ).catch((err) => {
      onError?.(err.message);
    });
    
    return () => {
      scanner.stop();
      scanner.clear();
    };
  }, [scanning]);
  
  return <div id="qr-scanner-region" />;
}

Scanner Configuration

facingMode
string
default:"environment"
Camera selection: "environment" for rear camera, "user" for front
fps
number
default:"10"
Frames per second for QR detection (10 FPS for optimal performance)
qrbox
object
default:"{ width: 250, height: 250 }"
Scanning area dimensions in pixels

URL Parsing

The system supports two QR code formats:

Format 1: Full Portal URL

try {
  const url = new URL(decodedText);
  const parts = url.pathname.split("/").filter(Boolean);
  // Expected: [org-slug, event-slug, "attendee", unique-id]
  if (parts.length >= 4 && parts[2] === "attendee") {
    await performCheckIn(parts[3]);
  }
} catch { /* fallback to format 2 */ }

Format 2: Unique ID Only

// If URL parsing fails, check if it's just the unique ID
if (/^[A-Z0-9]{6,10}$/i.test(decodedText.trim())) {
  await performCheckIn(decodedText.trim());
}
Both formats are supported for flexibility. Attendee portals generate full URLs, but manual codes can contain just the unique ID.

Check-In Methods Tracking

Each check-in records the method used:
const { error } = await supabase
  .from("attendees")
  .update({
    checked_in: true,
    checked_in_at: new Date().toISOString(),
    checkin_method: "qr_scan",  // or "self_service", "manual"
    portal_active: true
  })
  .eq("id", attendeeId);

Method Types

qr_scan

Scanned via QR code on public check-in page

self_service

Manual entry on public check-in page

manual

Manually checked in by event staff via Event Portal

Rate Limiting

The check-in endpoint includes rate limiting to prevent abuse:
src/pages/public/PublicCheckIn.tsx
if (fnError) {
  const msg = fnError.message;
  if (msg.includes("429") || msg.toLowerCase().includes("too many")) {
    toast.error("Too many requests. Please wait a moment and try again.");
    return;
  }
}
Rate Limit: Excessive check-in attempts from the same device will trigger a temporary block. Wait a few moments before retrying.

Post Check-In Experience

After successful check-in:
  1. Success Message: Customizable via branding templates
  2. Attendee Info: Display selected fields (name, table, seat, etc.)
  3. Portal Link: Direct link to attendee’s personal portal
src/pages/public/PublicCheckIn.tsx
<Card className="border-primary">
  <CardContent>
    <PostCheckinTemplate 
      branding={event.branding_postcheckin}
      variables={postCheckinVars}
    />
    <Button asChild>
      <a href={`/${orgSlug}/${eventSlug}/attendee/${attendeeUniqueId}`}>
        View My Portal
      </a>
    </Button>
  </CardContent>
</Card>

Camera Permissions

The scanner requires camera access:
1

Browser Prompt

Browser displays permission request when QR scanner is activated.
2

User Grant

User must allow camera access for scanning to work.
3

Error Handling

If denied, show error message and suggest manual entry option.

Permission Error Handling

src/components/QrScanner.tsx
if (cameraError) {
  return (
    <div className="border-destructive/50 bg-destructive/10">
      <CameraOff className="h-8 w-8 text-destructive" />
      <p>Camera Error</p>
      <p>{cameraError}</p>
      <Button onClick={onStop}>Close</Button>
    </div>
  );
}

Check-In Page Activation

Event Managers and Clients can toggle check-in page availability:
src/pages/event/CheckInManagement.tsx
const toggleCheckinPage = async () => {
  const newValue = !checkinPageActive;
  const { error } = await supabase
    .from("events")
    .update({ checkin_page_active: newValue })
    .eq("id", eventId);
    
  toast.success(
    newValue 
      ? "Check-in page activated" 
      : "Check-in page deactivated"
  );
};
Use Case: Disable check-in before the event starts or after it concludes to prevent early/late arrivals.

Manual Staff Check-In

Event staff can manually check in attendees from the Event Portal:
src/pages/event/CheckInManagement.tsx
const manualCheckIn = async (attendee) => {
  const { error } = await supabase
    .from("attendees")
    .update({
      checked_in: true,
      checked_in_at: new Date().toISOString(),
      checkin_method: "manual",
      portal_active: true,
    })
    .eq("id", attendee.id);
};

Build docs developers (and LLMs) love