One codebase serving two businesses with opposite requirements. The agency half exists to be found — every service page, product page and case study has to render for crawlers and win Google Ads quality scores. The HR half holds salary, attendance and employee records and must never be indexed, never leak through a shared layout, and never appear in a sitemap.
What I did about it
The split is structural, not conditional: exactly 52 of 104 pages are server components — the entire public marketing surface — and the other 52 are client-rendered behind auth. No page decides at runtime which it is.
The three SaaS products (Kaympus school ERP, Salexa CRM, Kayease Fintech) each get their own indexable route under /products/<slug> with its own schema.org category, so they compete as separate products in search rather than as sections of an agency site.
The employee portal and the marketing site share one Express + Mongoose backend but not one route tree — 25 route modules split along the same public/authenticated line as the frontend.
Attendance runs through a Flutter app rather than the web portal, because clocking in has to work on a phone at the office door, not on a laptop that is still in a bag.
What I rejected, and why
Two deployables would have isolated the HR data far more convincingly than one codebase with a disciplined boundary. Rejected because a five-person agency cannot maintain two release pipelines — and the boundary held: no authenticated route is server-rendered, and no public route reads an employee model. That is checkable in review, which is what makes it survivable.
One codebase serves the public marketing site and the internal HR portal. The public/authenticated boundary held, but two deployables would have isolated salary and attendance data structurally rather than by discipline. I traded isolation for a release pipeline a small team could actually maintain.