Skip to main content

Overview

The public-checkin edge function handles attendee check-ins from the public check-in page. It includes built-in rate limiting to prevent abuse and uses an atomic database operation to ensure check-in integrity.
This is a public endpoint (no authentication required). It implements aggressive rate limiting: max 10 check-ins per IP per 10-second window.

Endpoint

POST /functions/v1/public-checkin

Authentication

No authentication required. This endpoint is publicly accessible for self-service check-ins.

Rate Limiting

From source/supabase/functions/public-checkin/index.ts:12-44:
const WINDOW_MS = 10_000; // 10-second window
const MAX_REQUESTS = 10; // max 10 check-ins per IP per window

function isRateLimited(ip: string): boolean {
  const now = Date.now();
  let bucket = buckets.get(ip);
  if (!bucket) {
    bucket = { timestamps: [] };
    buckets.set(ip, bucket);
  }
  // Prune old entries
  bucket.timestamps = bucket.timestamps.filter((t) => now - t < WINDOW_MS);
  if (bucket.timestamps.length >= MAX_REQUESTS) {
    return true;
  }
  bucket.timestamps.push(now);
  return false;
}

Request Body

event_id
string
required
UUID of the event. Must be valid UUID format.
unique_id
string
required
Unique identifier of the attendee. Max 50 characters.
method
string
Check-in method. Defaults to "self_service". Max 50 characters.
email
string
Email address for verification (if required). Max 255 characters.

Example Request

curl -X POST 'https://<project-ref>.supabase.co/functions/v1/public-checkin' \
  -H 'Content-Type: application/json' \
  -d '{
    "event_id": "123e4567-e89b-12d3-a456-426614174000",
    "unique_id": "ATT-12345",
    "method": "qr_scan"
  }'

Example with Email Verification

curl -X POST 'https://<project-ref>.supabase.co/functions/v1/public-checkin' \
  -H 'Content-Type: application/json' \
  -d '{
    "event_id": "123e4567-e89b-12d3-a456-426614174000",
    "unique_id": "ATT-12345",
    "email": "[email protected]",
    "method": "manual_entry"
  }'

Response

Success Response (200 OK)

result
object
Result object from the atomic check-in operation. Contains check-in status and details.
{
  "result": {
    "success": true,
    "status": "checked_in",
    "attendee_id": "abc12345-e89b-12d3-a456-426614174001",
    "checked_in_at": "2026-03-04T10:30:00.000Z"
  }
}

Error Responses

400 Bad Request

Returned when request parameters are invalid.
{
  "error": "Invalid event_id"
}
{
  "error": "Invalid unique_id"
}
{
  "error": "Invalid email"
}

405 Method Not Allowed

Returned when using a method other than POST.
{
  "error": "Method not allowed"
}

429 Too Many Requests

Returned when rate limit is exceeded.
{
  "error": "Too many requests. Please wait a moment and try again."
}
Response includes Retry-After: 10 header indicating seconds to wait.

500 Internal Server Error

Returned when check-in operation fails.
{
  "error": "Check-in failed"
}
{
  "error": "Internal server error"
}

Implementation Details

IP Detection

From source/supabase/functions/public-checkin/index.ts:62-65:
const ip =
  req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
  req.headers.get("cf-connecting-ip") ||
  "unknown";

Input Validation

From source/supabase/functions/public-checkin/index.ts:86-109:
// Validate UUID format for event_id
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!event_id || typeof event_id !== "string" || !uuidRegex.test(event_id)) {
  return new Response(
    JSON.stringify({ error: "Invalid event_id" }),
    { status: 400 }
  );
}

if (!unique_id || typeof unique_id !== "string" || unique_id.length > 50) {
  return new Response(
    JSON.stringify({ error: "Invalid unique_id" }),
    { status: 400 }
  );
}

if (email && (typeof email !== "string" || email.length > 255)) {
  return new Response(
    JSON.stringify({ error: "Invalid email" }),
    { status: 400 }
  );
}

Atomic Check-in Operation

From source/supabase/functions/public-checkin/index.ts:111-123:
// Use service role to call the atomic RPC (bypasses RLS)
const supabase = createClient(
  Deno.env.get("SUPABASE_URL")!,
  Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);

const rpcParams: Record<string, unknown> = {
  _event_id: event_id,
  _unique_id: unique_id,
  _method: sanitizedMethod,
};
if (email) rpcParams._email = email;

const { data, error } = await supabase.rpc("public_atomic_checkin", rpcParams);
The public_atomic_checkin RPC function handles:
  • Verifying the event’s check-in page is active
  • Finding the attendee by unique_id
  • Optionally verifying email if provided
  • Atomically updating check-in status
  • Preventing duplicate check-ins

Security Features

  • Rate Limiting: In-memory sliding window rate limiter per IP address
  • Periodic Cleanup: Memory leak prevention with 60-second cleanup interval
  • UUID Validation: Strict validation of event_id format
  • Input Sanitization: Method string truncated to 50 characters
  • Service Role Bypass: Uses service role to bypass RLS for atomic operation
  • Atomic Operation: Database RPC ensures check-in integrity

Use Cases

  • Self-Service Check-in: Attendees check themselves in via QR code scan
  • Manual Entry: Staff manually enter unique_id for attendees
  • Email Verification: Optional email verification for additional security
  • Public Kiosk: Unmanned check-in stations at event entrances

Build docs developers (and LLMs) love