Case Study • Full-Stack Platform Engineering
Solomon Okeke Website Platform
A speaker's brand site that also sells tickets, takes bookings, and runs event check-in, not just a page describing what he does.
- Client
- Solomon Okeke: speaker, author, youth leadership consultant
- Role
- Sole engineer, architecture, backend, frontend, deployment
- Stack
- React 19 (Vite) • Express 5 • MongoDB • Node.js/Passenger
- Status
- Live in production visit ↗
01 Problem
A brand site that had to run a business, not describe one
Solomon runs paid and free events, takes speaking-engagement bookings, and manages a decade of event photography. None of it was centralized when this started.
Registration went through a generic Google Form with no payment collection built in. Booking requests came in by email with no visibility into whether a date was already taken, so double-booking was a real risk rather than a hypothetical one. Check-in at events meant a paper list. And several thousand event photos sat scattered across Drive folders from years of past programs, effectively invisible to the site.
The site needed to look like a speaker's brand and behave like the operations layer behind one.
02 Approach
The decisions that shaped it
server/models/Event.js · client/src/pages/AdminDashboard.jsx
A form builder instead of hardcoded fields
Every event used to need a code change to add a registration question. Now Event.formFields holds an array of question definitions; label, type, required flag, helper text, an optional "Other: ___" write-in, and staff build the form per event from the admin portal. Answers land in a flexible key-value map on each registration instead of fixed columns, which is what makes the per-event response dashboard possible: pie charts for single-choice questions, bar charts for checklists, generated from whatever questions actually exist on that event rather than a schema decided in advance.
Screenshot: Response Summary
server/routes/bookingRoutes.js • server/utils/calendarService.js
Bookings with real scheduling limits, and a calendar that syncs itself
The booking form doesn't just collect a date. It enforces two constraints, both server-side and client-side: five bookings maximum per day, and a two-hour minimum gap between any two so Solomon can actually travel between venues. Accepting a booking authenticates against Google's Calendar API through an OAuth2 refresh-token flow and creates the calendar event with reminders automatically. Decline or delete the booking and the calendar event goes with it, no orphaned entries left behind.
Payment for a booking runs the same logic one layer further. Accept a request with a price attached and the system generates a one-time token, gives it a 24-hour deadline, and emails the client a link. That link lives on a payment.solomonokeke.com.ng subdomain that's really the main site's own React Router, pointed at an unlisted /pay/:token route. A background sweep checks every 15 minutes for tokens whose deadline passed unpaid and flips those bookings to expired on its own.
Found during setup, not in code
DNS_PROBE_FINISHED_NXDOMAIN
The first attempt at the payment subdomain created it as a full addon domain with its own empty document root, silently spending one of the account's five domain slots and serving nothing. Recreated as an actual subdomain pointed at the main site's existing document root instead: unlimited on the plan, and it mirrors whatever's already deployed with no separate upload path, ever.
client/src/pages/CheckInPage.jsx
Rebuilding the QR scanner from the ground up
The check-in scanner originally ran on a popular third-party QR library. On certain Android/Chrome combinations, the camera feed would show for a second and then go solid black, camera technically active, nothing visible on screen. Patching the library's configuration didn't fix it. Replaced it entirely with a scanner built directly on the browser's own getUserMedia and BarcodeDetector APIs, with a jsQR canvas fallback for browsers without native barcode detection. Staff can also type the code by hand if a phone's camera won't cooperate that day.
server/models/Gallery.js • client/src/components/GalleryAlbumModal.jsx
A gallery that holds a decade of photos and stores none of them
Hosting years of event photography directly would have meant real storage and bandwidth cost on shared hosting. The gallery instead stores only Google Drive folder links per album. Images get fetched on demand through the Drive API with a public read-only key, so adding an album is sharing a folder link, not running an upload pipeline.
Getting there also meant re-curating 46-plus historical albums down to only photos that actually contain Solomon, since reviewing several thousand images by hand wasn't realistic. That ran through a Python face-recognition pipeline: DeepFace for matching, Drive OAuth for write access, images downscaled and processed in parallel to keep the runtime bearable.
server/models/Video.js
Video: the same logic, with YouTube instead of Drive
Videos store a YouTube ID and embed. Nothing self-hosted. Same reasoning as the gallery: playback, bandwidth, and delivery become YouTube's problem, and the admin side only manages titles, descriptions, and featured or short flags.
server/routes/registrationRoutes.js • server/routes/bookingRoutes.js
Payments, live, and the bug that only showed up in production
Event tickets and booking payments both run through Paystack: the public key drives client-side checkout, the secret key does server-side verification before anything gets marked paid. Cutting over from test keys to live keys surfaced a specific failure mode: "transaction reference not found" on verify, even though the customer had genuinely been charged. Paystack throws that exact error when the secret key verifying a transaction doesn't match the environment the reference was created in.
Diagnosed live, via cPanel terminal
node -e "console.log(process.env.PAYSTACK_SECRET_KEY)"
Run directly on the server to check what key the running process actually had loaded. It confirmed the live key was set in the hosting panel, but the running Node process still had the old test key cached: the environment variable had been added but never actually saved before the last restart. A second save and restart fixed it. A real payment went through clean right after.
03 Architecture
How it's actually put together
| Concern | Approach |
|---|---|
| Hosting | Shared cPanel hosting, Node.js Selector via Passenger, not a modern PaaS. That constraint shows up more than once below. |
| Routing config | Passenger's directives live in the same .htaccess file the frontend build ships. Redeploying the frontend used to silently wipe the config that made /api work. Fixed by moving those directives into client/public/.htaccess, so every build carries them forward automatically. |
| Deploy packaging | PowerShell's default zip tools write backslash path separators, which corrupt extraction on the Linux server. Zips get built by hand through System.IO.Compression.ZipArchive with explicit forward-slash paths instead. |
| Calendar & payments | Google Calendar via OAuth2 refresh token, Paystack via a public/secret key pair, both integrated server-side only. No client ever sees a secret key. |
04 Result
What changed
The measurable shifts since the system replaced the old form-and-email workflow:
Event operations
Registration, custom intake questions, payment, and QR check-in run from one system, with no manual reconciliation between them.
Booking scheduling
The two-hour-gap and five-per-day rules used to be checked by memory. Now they're enforced before a request can even be submitted.
Calendar upkeep
Accepted bookings appear on Solomon's calendar the moment they're accepted. No one retypes them.
Gallery cost
A decade of event photography, live on the site, at effectively zero storage cost.