Member & governance platform for a mass organization
One platform for the whole org: member database, live org-structure chart, agenda, news and an e-office letter workflow — plus a member portal with self-registration gated by admin approval. Deployed as a Dockerized stack behind a shared Caddy proxy with automatic HTTPS.
CONTEXT
A provincial chapter of a heritage membership organization needed to retire its manual processes. Member data lived in scattered spreadsheets, coordination across regional branches was ad-hoc, and event documentation was inconsistent.
I built a single Laravel + Inertia/React system split into three surfaces: a public SEO site (profile, news, agenda, regional map), an admin panel, and a member portal. Core modules include a member database with Excel import/export and photos, an interactive org-structure chart driven from live data, an agenda and calendar with reminders, news and documentation, and an e-office module for incoming and outgoing letters with automatic numbering, an approval flow, and soft-deleted archival.
Access is governed by role-based permissions across all three surfaces. Members can self-register from the public site, but login stays blocked until an admin approves them — enforced at both the login request and a session middleware, so revoked members are kicked out mid-session.
For delivery, the app is containerized with Docker (nginx, php-fpm, a queue worker, and the scheduler under supervisor) with MySQL, deployed onto a shared VPS behind a single Caddy reverse proxy with automatic Let's Encrypt HTTPS. It is hardened end to end: forced HTTPS, HSTS and security headers, secure cookies, upload-execution blocking, and CSV-injection-safe spreadsheet exports.
THE PROBLEM
The organization managed everything by hand. Member records were spread across disconnected spreadsheets with no single source of truth, coordination between regional branches happened over chat and paper, and activities — meetings, events, correspondence — were rarely archived anywhere retrievable. There was no public presence and no way for members to interact with the organization online.
CONSTRAINTS
Domain terms had to stay in Bahasa Indonesia to match how the organization actually works, while the code stayed English. Member NIK and personal data are sensitive and could never appear on public pages. Letters, decrees and decisions must remain auditable — never hard-deleted. And it had to run on a modest shared VPS already hosting other apps, so it couldn't claim its own ports or web server.
WHAT I DESIGNED
A single Laravel 13 + Inertia/React monolith split into three strictly separated surfaces — public SEO site, admin panel, and member portal — each with its own layout, routes and middleware. Data access goes through a repository layer and business logic through services. Role and permission checks guard every admin and portal route. Members self-register from the public site into a two-stage approval queue: the account is created but login is refused until an admin approves it, enforced both at the login request and by a session middleware. E-office handles letters with automatic numbering, an approval workflow and soft-deleted archival.
Key technical decisions
Every one is a tradeoff. Here's what I chose and what it cost.
One shared reverse proxy, not one per app
Dropped the app's own Caddy and put it behind a single shared proxy that terminates TLS for every app on the VPS, reaching the container over a shared Docker network.
Tradeoff: A strict deploy order (app up before the proxy reloads), traded for zero port conflicts and one place to manage certificates.
Approval enforced in two layers
Self-registration blocks login both at the login request and in a session middleware, so an approval revoked mid-session ejects the user on their next request.
Tradeoff: A permission check on every authenticated request, chosen over a single login-time gate that a live session could outlast.
Probe the database the way the app does
The container's wait-for-database probe uses PDO — the exact path Laravel connects through — instead of the bundled CLI client.
Tradeoff: A few more lines in the entrypoint, in exchange for no false failures: the Alpine mysql client is MariaDB's and rejects MySQL 8's self-signed TLS, which the app itself never hits.
Outcome
The organization now runs on one system instead of scattered files: a searchable member database with import/export, a live org-structure chart, an indexable public site, agenda and news, digital correspondence with approvals, and a member portal with QR cards, e-certificates and e-voting. It ships as a reproducible Docker image (nginx + php-fpm + queue + scheduler) and slots onto a shared VPS behind a single Caddy proxy with automatic HTTPS — adding the site was one config block, with zero downtime for the apps already running there.
STACK
Have a system that needs building — or fixing?
Tell me the problem and the constraints. I'll tell you honestly whether I'm the right person and how I'd approach it.