The Problem That Started It All
Event management in Nigeria and honestly across much of Africa, is still largely manual. Organizers uses spreadsheet, WhatsApp groups to confirm attendees. Check-in is done with printed lists and pens. Duplicate registrations go undetected until someone shows up claiming a seat that was already taken.Long queues of attendees in hot weather. Attendance data lives in Excel files that get emailed around after the event, if at all.
EventPro was built to solve exactly this problems. The goal was a single platform where an admin could manage organizers, organizers could manage their events, and attendees could register for events seemlessly, get confirmed, and check in with ease without long standing, โ all without a single WhatsApp message or printed list.
The core question: Can a self-taught developer with no team build a production-grade, multi-role SaaS platform entirely in vanilla JavaScript and Node.js?
Spoiler: yes. But it took longer, broke more, and taught me more than any course or tutorial ever has. This is that story.
Deciding the Architecture Before Writing a Line
The first week was entirely planning. No code. Just a lot of diagrams, questions, and second-guessing.
The three-role system โ Admin, Organizer, and Attendee was the foundational decision that shaped everything else. Each role needed:
- Its own separate dashboard with different navigation, data, and permissions
- Role-based API access โ an organizer should never touch admin data
- A shared auth system that routes users to the right dashboard after login
I debated using a framework. React would have made the component reuse cleaner. Next.js would have given me routing for free. I chose neither. The reason was deliberate: I wanted to prove to myself and to anyone looking at my portfolio, that I could build something production-grade without leaning on a framework as a crutch. Vanilla HTML, CSS, and JavaScript for the frontend. Node.js and Express for the backend. PostgreSQL for the database.
The file structure decision
With 20+ screens across three roles, the file structure had to be planned carefully or it would become a nightmare. I landed on this:
EventPro/
EventPro-Frontend/
โโโ assets/
โ โโโ images/ โ logos, illustrations, icons
โโโ js/
โ โโโ services/
โ โ โโโ auth-service.js โ login, logout, token storage, profile fetch
โ โโโ utils/
โ โ โโโ load-components.js โ role-aware sidebar + topbar injection
โ โโโ admin-dashboard.js
โ โโโ admin-events.js
โ โโโ admin-login.js
โ โโโ admin-report.js
โ โโโ attendees.js
โ โโโ create-event.js
โ โโโ csv-validation.js
โ โโโ duplicate-detection.js
โ โโโ forgot-password.js
โ โโโ organizer-dashboard.js
โ โโโ organizer-events.js
โ โโโ organizer-management.js
โ โโโ organizer-report.js
โ โโโ real-time-attendance.js
โ โโโ role-selection.js
โ โโโ settings.js
โ โโโ sign-in.js
โ โโโ signup.js
โ โโโ ticket-details.js
โ โโโ upload.js
โโโ pages/
โ โโโ admin-dashboard.html
โ โโโ admin-events.html
โ โโโ admin-login.html โ private URL, not linked publicly
โ โโโ admin-report.html
โ โโโ attendees.html
โ โโโ auth-reset-password.html
โ โโโ create-event.html
โ โโโ csv-validation.html
โ โโโ duplicate-detection.html
โ โโโ forget-password.html
โ โโโ organizer-accounts.html
โ โโโ organizer-dashboard.html
โ โโโ organizer-events.html
โ โโโ organizer-management.html
โ โโโ organizer-reports.html
โ โโโ real-time-attendance.html
โ โโโ role-selection.html
โ โโโ settings.html
โ โโโ sign-in.html
โ โโโ signup.html
โ โโโ sms-verification.html
โ โโโ ticket-details.html
โ โโโ upload.html
โโโ styles/
โ โโโ dashboard-layout.css โ shared sidebar + topbar shell
โ โโโ admin-events.css
โ โโโ attendees.css
โ โโโ csv-validation.css
โ โโโ organizer-events.css
โ โโโ real-time-attendance.css
โ โโโ settings.css
โ โโโ sign-in.css
โ โโโ signup.css
โ โโโ upload.css
โ โโโ ...
โโโ README.md
The key insight was auth-service.js โ a single file that
owns every API call in the entire frontend. No page ever calls
fetch() directly. This meant when the backend URL changed
(it did, twice), I updated one file.
Building the Backend First
This was the decision I'm most proud of. Most frontend developers build
the UI first and bolt on the backend later. I did the opposite. The
backend was entirely built and tested in Postman before a single
dashboard HTML file existed.
The backend runs on Node.js + Express, hosted on
Render's free tier, with a PostgreSQL database. The API
has Swagger documentation โ not because I had to, but because I'd seen
enough professional APIs to know that undocumented endpoints are a
collaboration nightmare.
The auth system
All three roles โ Admin, Organizer, Attendee โ share the same login
endpoint. The backend returns a JWT that contains the user's role. The
frontend reads that role and redirects accordingly. Simple in principle.
Surprisingly easy to get wrong in practice.
The first version had a bug where an organizer could modify the role in
their JWT and gain admin access. The fix was server-side role validation
on every protected route โ never trusting what the client sends.
Security lesson: Never trust the client. Always
validate role and permissions server-side on every single API call,
not just on login.
Social login via Appwrite
SMS and email verification were integrated through
Appwrite โ a backend-as-a-service that handles OTP
delivery reliably. This was a deliberate choice over building a custom
verification system. Building OTP delivery from scratch means managing
email deliverability, SMS gateway integrations, and rate limiting.
Appwrite handles all of that. Good engineers know when not to reinvent
the wheel.
The Features That Were Hard to Build
Real-time attendance tracking
This was the feature I was most nervous about. Real-time means the
dashboard updates live as attendees check in โ no page refresh required.
The implementation uses polling rather than WebSockets.
Every 10 seconds, the attendance dashboard silently fetches the latest
check-in data and updates the UI if anything changed. It's not as
elegant as a persistent WebSocket connection, but it's significantly
simpler to deploy on a free-tier server and handles the scale of events
this platform targets (tens to hundreds of attendees, not thousands).
The result: organizers see a live counter and attendee list that updates
in near-real-time without ever touching a refresh button.
CSV upload with duplicate detection
Bulk importing attendees sounds simple. It isn't. The requirements were:
- Accept a CSV file with arbitrary column names
- Map the columns to the expected fields (name, email, phone)
- Detect and flag duplicates before they hit the database
- Give the organizer a clear error report for every invalid row
- Import valid rows without stopping for the invalid ones
The duplicate detection runs at two levels: within the uploaded file
itself (someone adding the same email twice in the spreadsheet) and
against existing attendees in the database for that event. The frontend
shows a summary "47 imported successfully, 3 duplicates skipped, 1
invalid email format" before anything is committed.
The deactivation modal
A small feature that took disproportionate thought: when an admin
deactivates an organizer, what happens to their active events? The
decision was to cascade โ events remain published and accessible to
attendees, but the organizer loses dashboard access until reactivated.
The data doesn't disappear. This matters because real events have real
attendees who bought tickets or made plans.
The Frontend โ 20+ Screens in Vanilla JS
Building 20+ screens without a component framework means solving the
shared UI problem manually. The navigation sidebar appears on every
dashboard screen. In React, you'd use a layout component. In vanilla JS,
I built
load-components.js โ a utility that fetches shared HTML
fragments and injects them into a designated placeholder on each page.
The CSS architecture was equally deliberate.
dashboard-layout.css
defines every design token, shared component (cards, tables, badges,
modals, toasts), and responsive breakpoints. Each page imports it first,
then adds only its own page-specific styles. This means a button looks
identical on the admin dashboard and the organizer events page โ because
they're literally the same CSS class.
The design system
The purple sidebar (#6F00FF) was chosen intentionally.
Purple signals premium, authority, and trust โ the qualities an admin
dashboard should convey. The white active state on the sidebar link
creates a strong visual anchor that tells users exactly where they are
at all times.
Deployment and What Broke
The backend is deployed on Render's free tier. This
comes with one significant caveat: the server spins down after 15
minutes of inactivity. The first request after sleep takes 30โ60
seconds. This is why the auth service has a built-in timeout message
"The server may be starting up โ please try again" โ rather than a
generic error.
The frontend is deployed on GitHub Pages with a custom
landing page added later. The deployment issue that cost me an
afternoon: GitHub Pages is case-sensitive. Sign-In.html and
sign-in.html are different files on Linux but identical on
Windows. Every internal link had to be audited and corrected.
Deployment trap: Always develop on a case-sensitive
file system or explicitly lower-case every filename and every link
before deploying to Linux-based hosting.
What I'd Do Differently
Honest retrospective โ three things I'd change if I started today:
1. Use a module bundler from day one. With 30+
JavaScript files and no bundler, managing load order and avoiding global
scope pollution became a real problem by the end. Vite would have added
minimal overhead and solved this completely.
2. Write the database schema before the API. I wrote
the Express routes first and designed the PostgreSQL schema to fit them.
That's backwards. The data model should drive the API design, not the
other way around. I refactored two tables mid-build because of this.
3. Plan the mobile layout before building desktop. The
dashboard was designed desktop-first and mobile was retrofitted. It
works, but the mobile experience on the organizer management screen is
noticeably tighter than it should be. Mobile-first from the start would
have caught this.
What I'm Proud Of
Despite those lessons, EventPro is the project I'm most proud of in my
portfolio. Not because it's perfect โ it isn't โ but because it's real.
Real users have logged in. Real events have been created. Real check-ins
have happened.
The admin dashboard currently shows 17 published events and 83
registered users. Those aren't placeholder numbers. Those are actual
people who used something I built from nothing.
For a self-taught developer who started with HTML, building a platform
with multi-role JWT authentication, real-time data, CSV processing, and
a live PostgreSQL backend โ that's the proof of concept I needed to
prove to myself that I can build anything.
What's Next for EventPro
The immediate roadmap has three priorities: payment integration for
ticket sales, a mobile-responsive organizer dashboard, and moving off
Render's free tier to eliminate the cold start delay. Beyond that, QR
code check-in generation is planned โ instead of manual entry, attendees
get a unique QR code in their confirmation email that the organizer
scans at the door.
The codebase is open source. If you're a developer learning Node.js or
building your own SaaS, the source is on GitHub and the Swagger docs are
live โ dig in, borrow what's useful, and open an issue if you spot
something broken.