laravel-checkin maintained by mustafa-azmi
Laravel Checkin
Signed, time-limited, single-use check-in tokens for any Eloquent model — event attendance, classes, gym entry, coworking access, deliveries. QR-agnostic by design: pair it with whatever QR/barcode renderer you like, or don't use QR at all.
Originally generalized from a university attendance system (ScanTrack) into a package that works for any "someone needs to prove they showed up" use case.
Table of contents
Why this exists
Most check-in / attendance features get hand-rolled per project, with the same recurring mistakes:
- storing raw tokens that can be screenshotted or copied and reused indefinitely
- no expiry, so a leaked code works forever
- no protection against two people scanning/submitting the same code at the same instant and both being marked present
This package solves all three, and attaches to any model with one trait — verified with automated tests across PHP 8.2/8.3/8.4 and Laravel 10/11/12/13 on every push (see the badge above).
Installation
composer require mustafa-azmi/laravel-checkin
php artisan vendor:publish --tag=checkin-migrations
php artisan vendor:publish --tag=checkin-config # optional
php artisan migrate
Usage
Add the trait to any model that should be "checkinable":
use MustafaAzmi\Checkin\Traits\HasCheckins;
class Event extends Model
{
use HasCheckins;
}
Generate a token (e.g. when an attendee registers). This package doesn't render QR codes itself — pick any renderer you like and hand it the payload:
$generated = $event->generateCheckinToken(user: $attendee);
// $generated->raw is the ONLY time you'll ever see the raw token —
// it is never stored. Put it into a QR code with, e.g. endroid/qr-code:
$qrCode = \Endroid\QrCode\QrCode::create($generated->toQrPayload());
At the door, redeem the scanned/submitted token:
use MustafaAzmi\Checkin\Facades\Checkin;
use MustafaAzmi\Checkin\Exceptions\{
TokenNotFoundException,
TokenExpiredException,
TokenAlreadyUsedException,
};
try {
$token = Checkin::redeem($scannedValue);
// ✅ checked in — $token->tokenable is the Event, $token->user_id is the attendee
} catch (TokenNotFoundException) {
// not a valid code
} catch (TokenExpiredException) {
// expired — code was valid but too old
} catch (TokenAlreadyUsedException) {
// already checked in — flag as a possible duplicate/fraud attempt
}
Want to preview a code without consuming it (e.g. show attendee details on a
screen before staff confirms)? Use validate() instead of redeem() —
identical checks, but doesn't mark the token as used.
Design notes
- Tokens are HMAC-hashed, never stored raw. A full database leak cannot be used to forge or replay check-ins without also having
APP_KEY. - Redemption is wrapped in a locked transaction (
lockForUpdate), so concurrent scans of the same single-use code cannot both succeed — seetests/CheckinTokenServiceTest.phpfor the test proving this. - Polymorphic by design — one
checkin_tokenstable serves every model in your app that adopts the trait, rather than a bespoke table per feature. single_useis configurable per-token, not just globally — so the same package handles both "one-time event ticket" and "reusable gym door pass" patterns.- QR-agnostic — the package hands you a signed payload string; how you deliver it (QR image, barcode, plain text link, NFC) is entirely up to you.
Testing
composer install
vendor/bin/pest
CI runs the full suite against every combination of PHP 8.2/8.3/8.4 and Laravel 10.x/11.x/12.x/13.x on every push — see .github/workflows/tests.yml.
License
MIT — see LICENSE.md.