We replaced the patchwork with a single customer-service platform, shipped in three months on a fixed scope. The split is conventional and intentional: a public marketing site, a customer area sitting on top of an API, and internal staff tools built on a back-office tool extended in the right places.
The public site is the marketing surface — server-rendered for SEO, i18n where the business actually serves more than one language, no client-side JavaScript where a server response will do. It is the cheapest part of the system to operate and the one the customer sees first, so it gets the polish budget.
The customer area is where the work happens. A customer signs up, lands in an authenticated dashboard, completes a profile, opens a service request, uploads supporting documents, watches the status timeline update, and messages staff through a threaded conversation tied to the request. No more "did you get my email" exchanges. The state of every request is visible to the customer who owns it and the staff member assigned to it, in the same shape, at the same time.
The internal admin is a back-office tool extended where extension earns its keep — custom list views with the filters staff actually use, inline actions for triage and assignment, comment threads on every request, scheduled work attached to the customer record, reporting views that staff can pull without asking us. We did not build a separate "ops dashboard" application. The back-office tool, treated as a real product surface, covered ninety percent of what the team needed.
Underneath sits a relational database with a schema designed for the queries the business actually runs — partial indexes on open requests, semi-structured columns for the customer-specific fields that vary by service type, foreign keys everywhere a relationship is real. A background-job queue handles the things that should not block a request: email and push notifications for status updates, reminders for unanswered requests, nightly reports, document virus scans on upload.
Auth is session-based for staff and customers — no tokens floating around, no SaaS auth vendor. Role-based access distinguishes staff roles (admin, editor, viewer) from customer roles (account owner, delegated user). Every state-changing action writes to an audit log keyed by user and request, queryable from the admin. Search runs over database full-text indexes across customer history, scoped by role.
A payment provider is wired in for the paid services that needed it, behind the same background worker so webhook handling does not block the request thread. The integration is generic enough that swapping providers later is a contained change.