Offline-First Check-In: A Laravel API That Survives Venue Wi-Fi
TL;DR A gate check-in app can't depend on live Wi-Fi: scans must work offline and sync later. Four endpoints do it: manifest download, idempotent batch push, delta pull, online search. Client-generated UUIDs + a unique index make retries safe. Duplicates are a success status, not an error. The problem Physical event, staff scanning tickets at the door, venue Wi-Fi exactly as reliable as you'd expect. If your API sits in the hot path of every scan, the queue at the gate grows at the speed of the worst signal bar in the building. So the design flips the roles: the device owns check-in, the server owns convergence. Like a cashier who keeps a paper ledger when the till goes down — record now, reconcile later. The API surface Endpoint Purpose GET /staff/events/{uuid}/manifest paginated ticket snapshot, downloaded before gates open POST /staff/events/{uuid}/check-ins/batch push queued scans; safe to retry GET /staff/events/{uuid}/check-ins?since=<cursor> pull what other devices did GET /staff/events/{uuid}/participants?q= online fallback search (lost ticket, typo) The sync loop Device Server |--- GET manifest (before event) ------->| | scan offline, queue locally | |--- POST batch [{client_uuid, ts}] ---->| dedupe on client_uuid |<-- 200 {applied | duplicate per item} -| |--- GET check-ins?since=cursor -------->| scans from other devices |<-- delta + next cursor ----------------| Idempotency is the whole trick Every scan gets a UUID generated on the device at scan time . The server puts a unique index on it and inserts-or-ignores: public function batchCheckIn ( BatchCheckInRequest $request , string $uuid ): JsonResponse { $results = collect ( $request -> validated ( 'check_ins' )) -> map ( function ( array $scan ) { $checkIn = CheckIn :: firstOrCreate ( [ 'client_uuid' => $scan [ 'client_uuid' ]], [ 'ticket_id' => /* resolved from scan */ , 'checked_in_at' => $scan [ 'scanned_at' ]], // ... ); return [ 'client_uuid' => $scan [ 'client_uuid' ], 'status' => $checkIn -> wasR