Building a Self-Hosted Event QR Code Scanner: A Complete Guide
From Zero to Production in One Weekend
In the last 3 years I have volunteered for 10+ events from Folk Fest to Burning Man. I often do Gate + Check-in and its a pain to scan people in by hand or worse a paper list! Long lines, tired people, people spelling their names three times? Yeah, welcome to hell.
So I decided to build my own QR code ticket scanner system that runs on my own server. I'm going to be using this system for an upcoming Women's Festival that until now did all check in by hand on paper. Full disclosure I vibe-coded this with Claude but it works and its free, for a small event of less than 1000 attendees this will do the job just fine.
Security Disclaimer: This tutorial uses example configurations for educational purposes. Always use strong, unique passwords, keep your software updated, and follow your hosting providers security recommendations. The security practices shown here are industry-standard but should be adapted to your specific needs.
What We're Building
A professional event check-in system with:
🎫 QR code scanning via phone camera
👥 Manual search functionality (for when QR codes don't work)
📊 Real-time check-in tracking
🔐 Secure multi-user access (multiple staff members scanning simultaneously)
💾 Self-hosted (you own your data!)
Tech Stack:
YunoHost (for server management)
NocoDB (for the database/admin panel)
PHP (for the scanner app)
MySQL (for data storage)
Time to build: ~6 hours (including learning and debugging)
Cost: Free (if you already have a server) or ~$5/month for a VPS
Why Self-Host?
Before we dive in, you might ask: "Why not just use Eventbrite or some SaaS?" Fair question!
Reasons I chose self-hosting:
Data ownership - All attendee info stays on my server
No per-ticket fees - SaaS tools charge per ticket (adds up fast!)
Customization - I can modify anything I want
Learning - Great project to understand full-stack development
Control - No internet? No problem (with the right setup)
Part 1: Setting Up Your Server with YunoHost
What is YunoHost?
Think of YunoHost as "WordPress for servers" - it makes running a server as easy as using a web interface. No need to memorize Linux commands (though we'll use some!).
Step 1: Install YunoHost
If you don't have YunoHost yet:
Get a VPS (I use DigitalOcean, but Linode, Hetzner, or any provider works)
Follow YunoHost installation guide
Complete the post-installation setup
Add your domain (e.g.,
yourdomain.com)
Cost: $5-10/month for a basic VPS
Step 2: Install the "My Webapp" App
YunoHost has an app catalog. We need "My Webapp" - it's basically a blank canvas PHP app.
During installation, it'll ask:
Domain:
scanner.yourdomain.com(or whatever subdomain you want)Path:
/(just press enter)Admin user: Choose who can access it
What you get:
A working web server at
https://scanner.yourdomain.comPHP 8.4 pre-configured
MySQL database ready to use
SSL certificate (automatic HTTPS)
Pretty cool, right?
Part 2: Setting Up NocoDB (Your Database)
Why NocoDB?
NocoDB is like "Airtable but open-source and self-hosted." It gives you:
A spreadsheet-like interface for your data
Built-in QR code generation
API access (so your scanner can read/write data)
No coding required for data management
Step 1: Install NocoDB on YunoHost
Choose:
Domain:
nocodb.yourdomain.comAccess: Private (protect it with login)
Step 2: Create Your Ticket Database
Go to
https://nocodb.yourdomain.comCreate a new project called "Event Tickets"
Create a table called "Attendees"
Add these columns:
Ticket_ID(Single Line Text) - e.g., "TKT-2025-0001"Attendee_Name(Single Line Text)Attendee_Email(Email)Attendee_Phone(Phone)Ticket_Type(Single Select: VIP, Regular, Vendor)Scanned(Checkbox) - Track if they checked inScanDate(Date)ScanTime(Time)
Add a QR Code column that uses
Ticket_IDas the value
Pro tip: NocoDB auto-generates QR codes! You can download them and send to attendees.
Step 3: Get Your API Token
Click your profile (top right)
Go to "API Tokens"
Create a new token
Copy it somewhere safe (you'll need it soon)
Part 3: Building the Scanner App
Now for the fun part! We're building a PHP web app that:
Scans QR codes using your phone's camera
Checks tickets against the NocoDB database
Marks people as checked in
Shows success/error messages
The Architecture
Key features we'll implement:
Continuous scanning - Scan, show result, ready for next person (no stopping!)
Visual feedback - Green border for success, red for errors
Manual search - Search by name when QR codes don't work
Multi-user support - 4 scanners can work simultaneously
Race condition prevention - Two people can't check in the same ticket at once
Security - Database credentials hidden from web
The Code Structure
I won't paste the entire code here (it's 1000+ lines!), but here's the architecture:
index.php has four main sections:
1. User Authentication
Uses PHP sessions to keep users logged in. Passwords are hashed with password_hash() for security.
2. Database Connection
3. API Endpoints (AJAX handlers)
4. Frontend (HTML + JavaScript)
Key Technical Challenges Solved
Challenge 1: Continuous Scanning
Problem: Most QR scanners stop after each scan. Not great for events!
Solution: Instead of stopping the camera, we just:
Flash a green/red border for visual feedback
Show the result message
Let the scanner keep running
Next scan replaces the old message
Challenge 2: Race Conditions
Problem: What if Scanner 1 and Scanner 2 scan the same ticket at the exact same time?
Solution: Database row locking with transactions!
The FOR UPDATE clause locks the row. If Scanner 2 tries to access it, they wait until Scanner 1 finishes.
Challenge 3: Mobile Responsiveness
Problem: Staff use phones to scan. Desktop-first designs don't work.
Solution: Mobile-first CSS with proper viewport and touch handling:
Part 4: Security - Protecting Your Database Credentials
Here's where it got interesting. Initially, I had database credentials directly in index.php:
The problem: If PHP fails or someone views the source, they see your password!
The Secure Solution
Step 1: Create a config directory OUTSIDE the web root
Directory structure:
Step 2: Create database.php with credentials
Step 3: Lock down permissions
This is crucial! We need to make the file readable ONLY by the PHP process.
What chmod 400 means:
4= read for owner0= no permissions for group0= no permissions for others
Result: Only the YOUR_APP_NAME user can read it!
Step 4: Fix directory permissions
Step 5: Add .htaccess protection as backup
Step 6: Load config in your app
Testing Security
Try accessing:
https://scanner.yourdomain.com/config/database.php→ Should fail (403/404)https://scanner.yourdomain.com/../config/database.php→ Should fail
Try reading the file:
Security layers:
✅ File outside web root (not accessible via URL)
✅ Restrictive permissions (chmod 400)
✅ .htaccess protection (backup layer)
✅ Database on localhost only (not accessible remotely)
✅ HTTPS encryption (all traffic encrypted)
Part 5: Deploying and Testing
Deployment Checklist
Testing Your Scanner
Test 1: Basic Login
Go to
https://scanner.yourdomain.comLogin with your credentials
Should see the scanner interface ✓
Test 2: QR Scanning
Click "Start Scanner"
Allow camera access
Point at a QR code (create test tickets in NocoDB)
Should see green border flash
Success message appears
Scanner stays active for next person ✓
Test 3: Manual Search
Click "🔍 Search" tab
Type part of an attendee's name
Should see matching results
Click checkbox to check them in ✓
Test 4: Multi-User (Race Conditions)
Open scanner on two phones
Both scan the SAME ticket simultaneously
First one: ✅ "Check-in successful!"
Second one: ⚠️ "Already checked in by scanner1" ✓
Test 5: Security
Try
https://scanner.yourdomain.com/config/database.php→ Should fail ✓View page source → Search for your password → Should NOT find it ✓
Lessons Learned & Tips
What Went Wrong (So You Don't Have To)
1. CORS Issues Initially tried to have the scanner call NocoDB's API directly. Hit CORS (Cross-Origin Resource Sharing) issues because they're on different domains.
Solution: Connect directly to MySQL instead. Simpler, faster, no CORS!
2. YunoHost SSO Blocking API Calls YunoHost has a Single Sign-On system that was blocking my API proxy attempts.
Solution: Again, direct MySQL connection solved it.
3. File Permission Confusion Spent an hour debugging why PHP couldn't read the config file. Turned out PHP was running as YOUR_APP_NAME user, not www-data!
Solution: Always check what user your PHP-FPM pool runs as.
4. Scanner Stopping After Each Scan Original implementation stopped the camera after each scan. Super annoying at busy check-in.
Solution: Just reset the isProcessing flag instead of stopping the camera.
Pro Tips
1. Test with fake data first Don't test with real attendee data. Create 10 fake tickets in NocoDB first.
2. Have a backup plan Print a paper list. Technology fails. Have a manual backup.
3. Charge your devices Bring power banks. Scanners drain battery fast.
4. Test in the actual venue WiFi might be spotty. Internet might be slow. Test beforehand!
5. Train your staff 15 minutes of training prevents hours of confusion.
6. Monitor in real-time Keep NocoDB open on a laptop to watch check-ins happen live.
Going Further
Features I'd Add Next
1. Offline Mode
Cache tickets in localStorage
Queue check-ins
Sync when connection returns
2. Analytics Dashboard
Real-time check-in counter
Chart showing check-ins over time
VIP vs Regular breakdown
3. QR Code Generation
Auto-generate tickets in bulk
Email QR codes to attendees
PDF ticket generation
4. Check-Out Tracking
Track when people leave
Calculate average stay time
5. Export Functionality
Export attendance to CSV
Generate attendance report
Scaling Up
This setup easily handles:
✅ 500+ attendees
✅ 4-5 simultaneous scanners
✅ Multiple events (just create new NocoDB tables)
For larger events (1000+ attendees):
Use Redis for caching
Add a load balancer
Move MySQL to a dedicated server
Cost Breakdown
Initial Setup:
VPS (2GB RAM): $5-10/month
Domain: $10/year
SSL Certificate: Free (Let's Encrypt)
Total: ~$5-10/month
Compare to SaaS alternatives:
Eventbrite: 3.5% + $1.79 per ticket
For 500 tickets at $20 each: $1,245 in fees!
Your system pays for itself after ONE event.
Conclusion
Building your own event check-in system is:
✅ Cheaper than SaaS (no per-ticket fees)
✅ More secure (you own the data)
✅ More flexible (customize anything)
✅ Educational (learn PHP, databases, security)
✅ Reliable (no vendor lock-in)
The whole project took me about 6 hours including:
Learning YunoHost
Setting up NocoDB
Writing the scanner code
Debugging security issues
Testing everything
Was it worth it? Absolutely! I now have:
A reusable system for future events
Complete control over my data
No ongoing per-ticket fees
A cool project for my portfolio
Resources
Code & Deployment Scripts:
Full source code: [GitHub link would go here]
Deployment automation script
Security hardening guide
Official Documentation:
Helpful Communities:
YunoHost Forum
r/selfhosted on Reddit
NocoDB Discord
Your Turn!
Have questions? Hit me up in the comments!
Thinking of building this? Here's your weekend project checklist:
[ ] Set up YunoHost server
[ ] Install My Webapp
[ ] Install NocoDB
[ ] Create ticket database
[ ] Build scanner interface
[ ] Secure your config
[ ] Test with fake data
[ ] Deploy and use!
Time investment: One weekend
Skill level: Intermediate (basic PHP and command line knowledge helpful)
Satisfaction: 🎉🎉🎉
Happy scanning! 🎫
Built something cool with this guide? I'd love to see it! Tag me or drop a link in the comments.


