Because sometimes the best database… is Google Sheets.
If you work in tech long enough, you’ll eventually get that one project where someone asks:
“Can we check RORO ferry eligibility… online… instantly… and make it look simple?”
Simple?
Bro. Behind every “simple” public form is a hundred ways for users to break it.
But Langkawi Bike Fest 2025 needed a fast, public-facing RORO lookup system so participants could check whether their motorcycle + registration qualified for ferry service. And they needed it fast, pretty, secure, and accurate.
So yes, I built it.
And here’s how the whole thing went down.
The Problem: Google Sheets + High Traffic = Recipe for Chaos
The organizers were storing all registrations inside Google Sheets. Great for humans.
Not great for performance, structure, or dealing with thousands of queries like:
-
“Is my bike eligible for RORO?”
-
“What time is my ferry?”
-
“Why my friend got complimentary but I got paid service?”
-
“Bro, the sheet loaded but where’s my name?”
The system needed to:
✔ Accept any vehicle plate format (WKH9737 / WKH 9737 / lowercase / uppercase)
✔ Pull data in real-time from Google Sheets
✔ Protect against bots
✔ Handle multiple users under one vehicle (rider + passenger setups)
✔ Work smoothly on mobile
✔ Never expose sensitive data
✔ Avoid API rate-limit meltdowns
Challenge accepted.
With kopi.
The Tech Stack: Lightweight but Powerful
Here’s what I used to build the machine:
Backend
-
Laravel 11
-
PHP 8.2
-
Google Sheets API v4
-
Docker for deployment consistency
Frontend
-
TailwindCSS
-
Blade Templates
-
Vanilla JS (no React because I respect my page load time)
Security
-
reCAPTCHA v3
-
Rate limiting (10 requests/minute)
-
CSRF protection
-
Sanitization for all inputs
-
No dangerous data exposed
Simple, modern, fast.
Architecture: Clean, Modular, and Not Headache-Inducing
A clean service-oriented architecture kept it easy to maintain:
Browser → Controller → GoogleSheetService → Google Sheets
CheckController
-
Validates plate number
-
Sanitizes input
-
Checks reCAPTCHA
-
Applies rate limits
-
Handles session + redirect flow
GoogleSheetService
-
Fetches the sheet data
-
Caches the result for 60 minutes
-
Normalizes plate numbers
-
Handles multi-tab sheets
-
Matches columns even if organizer renamed something dramatic
Smarter Plate Matching Than Some Humans
People type plate numbers in all kinds of creative formats.
So I normalized everything:
"WKH 9737" → "wkh9737" "Wkh 9 7 3 7" → "wkh9737" " WKH9737 " → "wkh9737"
Case-insensitive, space-insensitive, nonsense-resistant.
This alone saved 50% of potential support calls.
Multi-User Display: Because One Bike ≠ One Human
Some bikes come with:
-
Rider
-
Passenger
-
Passenger’s cousin
-
Passenger’s cousin’s helmet
So the system supports multiple rows under the same vehicle:
-
Pagination (User 1 of 3)
-
Smooth transitions
-
Keyboard arrow navigation
Everything clean and intuitive.
RORO Eligibility Logic: Straightforward and Transparent
Based strictly on the “Need RORO?” column:
| Sheet Value | Meaning |
|---|---|
Yes |
Complimentary (Eligible) |
Yes (Additional Charge RM150) |
Paid Service (Eligible) |
No |
Not Eligible |
No. I will handle myself |
Not Eligible |
| Blank | Not Eligible |
Only eligible users see extra ferry details like departure time, location, etc.
Security: Invisible but Serious
The system may look simple on the surface, but under the hood:
-
ReCAPTCHA v3 blocks bots
-
Input regex checks block weird characters
-
Rate limiting stops spammers
-
All dangerous output escaped
-
Session protection & CSRF built-in
-
Error messages sanitized
This thing is more secure than some banking portals I’ve seen.
Performance: From 1200ms → 45ms
Before caching:
-
1–2 seconds per lookup
-
Google Sheets API crying
-
High risk of rate limit hits
After caching:
-
45ms average lookup speed
-
95% sheet API reduction
-
Lower server load
-
Zero angry users
Caching always wins.
🐳 Deployment: Docker Makes Life Beautiful
One docker-compose file and boom — portable, predictable, deployable anywhere.
Easy to scale, easy to move between dev > staging > prod.
Monitoring handled by:
-
Laravel logs
-
GA4
-
Server-level tracking
What I Learned (and What You Should Steal)
1. Google Sheets CAN be a database… with discipline.
Perfect for human-managed data. Terrible without caching.
2. Real-time apps don’t need heavy frameworks.
Laravel + vanilla JS = fast website + happy phone users.
3. Security must be baked in from the start.
Retrofit security = regret.
4. UX is the secret sauce.
Tiny things matter:
-
Arrow-key navigation
-
Clean pagination
-
Loading states
-
Mobile responsiveness
5. Logging saves your life.
Especially when users swear “I typed the correct plate number.”
Final Result
The RORO eligibility checker is now:
-
Fast
-
Secure
-
Reliable
-
User-friendly
-
Real-time
-
Event-proof
-
Organizer-friendly
-
Rider-approved
And best of all…
The organizers update Google Sheets, and the app updates itself.
No code deployments. No drama.
Closing Thoughts
This project proves that with the right architecture, even a simple Google Sheet can power a clean, modern, production-grade lookup system.
If Langkawi Bike Fest 2025 needed a real-time, public-facing RORO checker again tomorrow…
This system is ready.
And if you need one built for your event?
Bring coffee.
We’ll talk.