A real-time event check-in system with QR codes, offline-first scanning, and AI-powered insights.
- Backend: Flask, PostgreSQL, SQLAlchemy, Flask-JWT-Extended, Flask-SocketIO, Flask-Limiter
- Frontend: React (Vite), TailwindCSS, html5-qrcode, qrcode.react, Socket.IO client
- AI & Integrations: Google Gemini API
- Infrastructure: Docker Compose, Redis (for distributed rate limiting)
The project is completely containerized. You can get the whole system running on a clean machine using Docker.
Prerequisites: Docker and Docker Compose installed.
- Clone the repository (if not already done).
- Setup Environment Variables:
Important: Open
cp .env.example .env
.envand fill inGEMINI_API_KEY. Note: If you do not wish to set up a Gemini key, you can leave it blank or invalid. The application will still run perfectly; the AI insights feature will simply fall back to displaying raw backend statistics. - Start the application:
docker-compose up --build
Service Ports:
- Frontend: http://localhost:3000
- Backend API:
http://localhost:5000 - PostgreSQL Database:
5432 - Redis:
6379
Note: Database migrations run automatically on startup via the backend's entrypoint script. No manual DB setup is required.
There is no pre-seeded data, but creating accounts is quick and easy. Note that all accounts must use a valid student email domain: @vitstudent.ac.in (this is enforced, but not verified against a real directory, so any made-up address ending in this domain will work).
To test the system, create two accounts manually via the UI at http://localhost:3000/register:
- Organizer Account: Select "Organizer" role, select a club, and enter the organizer signup code:
1309. - Attendee Account: Log out, go back to register, and create an "Attendee" account.
Follow this path to see all core requirements in action:
- Create Event: Log in as the Organizer, go to the dashboard, and create an event with a small capacity (e.g., 5).
- Register Attendees: Log out, log in as your Attendee, go to the event page, and register for it. Note the generated QR code on your registrations page.
- Live Scan: Log out, log back in as the Organizer. Open the Scanner page. Scan the attendee's QR code (you can point your phone at the screen or use a webcam). You should see a successful check-in.
- Duplicate Prevention: Scan the exact same QR code again. The system will reject it with an "already checked in" conflict.
- Real-time Dashboard: Open the Event Dashboard in one tab and the Scanner in another. Scan a new attendee (or simulate one) and watch the dashboard counts and recent activity update instantly without refreshing.
- Offline Sync: Use your browser's DevTools (Network tab) to throttle to "Offline". Scan a QR code. It will queue locally. Turn the network back to "Online" and watch the app automatically sync the pending scans to the backend.
- AI Insights: Go to the Event Dashboard and ask the AI insights panel a natural language question (e.g., "How is our turnout looking?").
- Export Data: Click the "Export CSV" button on the Event Dashboard to download a full list of attendees and their check-in timestamps.
Here is exactly how this repository fulfills the hard technical constraints:
The backend uses ACID-compliant transactional guarantees and atomic database updates to prevent race conditions during both registration (capacity) and check-ins (duplicates).
- Proof: Two concurrency scripts are provided in the
scripts/folder:test_concurrency.py(for registration capacity) andtest_checkin_concurrency.py(for check-in duplicate prevention). - Run the proof: With the app running via
docker-compose up, open a terminal and run:The script registers an attendee, gets their QR token, and then blasts thepip install aiohttp # if not installed globally python scripts/test_checkin_concurrency.pyPOST /api/checkinendpoint with 50 simultaneous requests. Expected Output: You will see exactly 1 request succeed (200 OK) and 49 requests fail (409 Conflict). The database count increments by exactly 1.
- Proof: We implemented a one-time use opaque bearer token (
qr_token) stored in theRegistrationtable. Once an attendee is checked in, the system enforces a strict 1:1 mapping. If the attendee shares a screenshot of their QR code to a friend, the scanner will recognize the token has already been consumed and reject the second scan with a 409 Conflict.
-
Proof: The
ScannerPage.jsxusesidb-keyval(IndexedDB) to cache scans locally if the network drops or a check-in request fails. Each scan generates a uniqueclient_scan_id. When connectivity is restored, the queued scans are sent toPOST /api/checkin/sync. -
Conflict Policy: Conflict resolution is based on server-arrival order, not the
device_scanned_attimestamp. Each queued scan goes through the same transactional check-in logic used for normal online scans. Whichever request successfully acquires the required row lock and commits first wins.If another offline station attempts to sync the same QR code later—even if its
device_scanned_attimestamp is earlier—the scan is rejected as a conflict. The losing scan is reported in the sync response and logged to theCheckInConflictaudit table. Conflicts are handled gracefully without crashing or interrupting the rest of the batch sync. -
Result: This ensures that duplicate check-ins from multiple offline scanners are resolved safely and consistently, while preserving an audit trail of conflicting attempts.
- Proof: Implemented using the
google-genaiSDK inbackend/app/blueprints/insights.py. The backend first computes hard statistics directly from the database (registered count, checked-in count, capacity) and injects them into a strict system prompt. The AI is instructed to only explain and interpret these provided numbers, never to calculate them itself. - Verification: You can verify the graceful fallback by changing the
GEMINI_API_KEYto an invalid string in.envand restarting the backend. The panel will cleanly fall back to showing raw JSON stats without crashing.
| Variable | Description | Required | Default |
|---|---|---|---|
DATABASE_URL |
PostgreSQL connection string | Yes | postgresql://postgres:postgres@db:5432/eventcheckin |
POSTGRES_USER |
DB Init User | Yes | postgres |
POSTGRES_PASSWORD |
DB Init Password | Yes | postgres |
POSTGRES_DB |
DB Init Database Name | Yes | eventcheckin |
FLASK_ENV |
Environment mode | No | development |
JWT_SECRET_KEY |
Secret for signing JWTs | Yes | change-me-in-production-use-a-long-random-string |
ORGANIZER_SIGNUP_CODE |
Code required to register as organizer | Yes | 1309 |
GEMINI_API_KEY |
Google Gemini AI Key | No | (Empty - falls back to raw stats) |
VITE_API_URL |
API base URL for frontend | Yes | http://localhost:5000 |
RATELIMIT_STORAGE_URI |
Redis URL for rate limiting | Yes | redis://redis:6379/0 |
- First-Use Impersonation: While the one-time token prevents multiple people from entering on the same ticket, it does not prevent someone from stealing a ticket and entering first. True identity verification would require checking a student ID card at the door alongside the scan.
- JWT Storage: The frontend currently stores JWTs in
localStoragefor simplicity and development speed. In a strict production environment, anhttpOnlysecure cookie would be preferred to mitigate XSS risks, though this adds significant CORS/CSRF complexity.
.
├── backend/
│ ├── app/
│ │ ├── blueprints/ # API Routes (auth, events, checkin, registrations, insights)
│ │ ├── models.py # SQLAlchemy DB models
│ │ ├── extensions.py # Shared Flask extensions (DB, Limiter, SocketIO)
│ │ └── sockets.py # SocketIO event handlers
│ ├── migrations/ # Alembic DB migrations
│ ├── requirements.txt
│ └── wsgi.py # Application entrypoint
├── frontend/
│ ├── src/
│ │ ├── components/ # Reusable UI (Navbar, Cards)
│ │ ├── context/ # Auth Context
│ │ └── pages/ # Page Views (Dashboard, Scanner, Events, Auth)
│ ├── vite.config.js
│ └── package.json
├── scripts/ # Concurrency testing and proof scripts
└── docker-compose.yml # Multi-container orchestration