Architecture
How the MotorPH Enterprise Payroll System fits together: what runs, why each piece exists, how the pieces talk to each other, and where authentication and authorization happen. Deep dives live in frontend/, backend/, security/, and deployment/; the database schema is in erd.md.
1. High-level overview
| Service | Image | Host port | Why it exists |
|---|---|---|---|
motorph_payroll_db | postgres:16 | 5434 → 5432 | Primary datastore. Schema owned entirely by Flyway (V1–V88); Hibernate runs ddl-auto: validate and never generates schema. |
motorph_payroll_backend | backend/Dockerfile | 8081 → 8080 | The REST API and the only process with DB access. Also serves the STOMP WebSocket (/ws), Swagger UI, and Prometheus metrics. |
motorph_payroll_frontend | frontend/Dockerfile | 5173 → 80 | nginx serving the static React build and reverse-proxying /api/ + /ws to the backend, so the browser talks to a single origin and there are no CORS hops in normal use. |
motorph_payroll_pgadmin | dpage/pgadmin4:9.16 | 127.0.0.1:5050 → 80 | Development database browser, no login screen. Registers the owning role and app_runtime separately so the effect of row-level security is visible (backend/pgadmin.md). |
motorph_payroll_mailpit | axllent/mailpit:v1.30 | 127.0.0.1:8025 → 8025 | Development mail catcher on SMTP 1025 (not published). Nothing it receives leaves the machine, which matters because welcome emails carry one-time passwords (backend/email.md). |
| observability stack (12 containers) | overlay: docker-compose.monitoring.yml | 127.0.0.1:3000 (Grafana), 9090 (Prometheus), 9093 (Alertmanager) | Optional but production-ready: Prometheus + Alertmanager, Loki + Promtail, Tempo + OpenTelemetry Collector, Grafana with 9 provisioned dashboards, and exporters for host, containers, PostgreSQL, TLS/uptime and nginx (deployment/monitoring.md). |
Ports are the dev-compose defaults (overridable via FRONTEND_PORT,
BACKEND_PORT, POSTGRES_PORT, PGADMIN_PORT, MAILPIT_UI_PORT — see
reference/env-vars.md). pgAdmin and Mailpit are
development-only and bound to loopback: neither has any authentication, and
neither appears in the per-client production template.
docker compose up -d --build builds everything, applies all Flyway
migrations on backend startup, and auto-seeds demo data (§6)
so the app is demoable with no manual setup.
In production the same two app images run once per client behind a shared Traefik edge — there is no multi-tenant database. See deployment/vps-guide.md and ADR-0007.
One frontend, three experiences
The single React app serves three route trees (frontend/routing.md):
- ERP app (authenticated) — HR, payroll, recruitment/ATS, CRM, inventory, self-service, billing, admin.
- Public marketing site — home/pricing/FAQ plus public careers pages that feed the recruitment module.
- Customer portal — a storefront (catalog/cart/orders) with its own, separate auth system (see below).
2. Frontend (summary)
Stack: React 19 + TypeScript + Vite, Chakra UI v3, AG Grid Community, Redux Toolkit (auth only) + TanStack React Query (server state) + Zustand (portal/UI islands), React Router 7, axios, React Hook Form + Zod.
- All server data flows through React Query hooks in
hooks/api/(~52 modules) wrapping thin axios modules inapi/(~60 modules). frontend/state.md explains why state is split three ways. api/client.tsbuilds the axios instance.VITE_API_URLdefaults to empty, meaning relative URLs — every request goes through the nginx proxy on the same origin. It is baked in at image build time (frontend/api-layer.md).- Every data table is built on the in-house enterprise grid subsystem
(
ui/ag-grid/— server-side infinite row model, tabs, saved views, bulk actions, import/export) — frontend/ag-grid.md. - Route guarding mirrors backend permissions:
ProtectedRoute(login) +RequirePermission(permission strings identical to the backend's@PreAuthorizeconstants). Hiding UI is UX only — the backend check is the security boundary (frontend/auth-and-permissions.md).
3. Backend (summary)
Stack: Java 21, Spring Boot 4.0.7, Maven, PostgreSQL 16, Flyway,
springdoc-openapi, Micrometer/Prometheus, Auth0 java-jwt, Bucket4j, ALTCHA.
Classic layered architecture under com.motorph.payroll:
controller (78) → service / service.impl → repository (+ specification) → model (85 JPA entities)
DTOs sit at the controller boundary; entities never leave the service layer.
Mapping is mostly hand-written (MapStruct is used for exactly two mappers —
see backend/README.md). Dynamic AG-Grid-style
filter/sort/pagination is implemented with Spring Data Pageable +
Specification classes. Errors are centralized in GlobalControllerAdvice
with a consistent ApiExceptionResponse shape
(api/conventions.md).
Domain modules (by class-name prefix): Employee/Department/Position, Timesheet, Leave, Overtime, Reimbursement, Payroll/Payslip + statutory rates, EWT & government forms, Bonus/Deductions, Holidays & work suspensions, Recruitment/ATS, CRM, Inventory/Warehouse, Customer Portal, Billing (Polar.sh), Announcements/Notifications/Chat (WebSocket), RBAC & audit, Reporting/analytics. The payroll computation engine and its PH-compliance rules are documented in business-rules.md.
4. Where authentication and authorization happen
Full detail: security/authentication.md and security/request-pipeline.md.
- Authentication happens in the backend filter chain, per request:
LoginRateLimitFilter(Bucket4j, 429) →AltchaVerificationFilter(proof-of-work CAPTCHA, 428) →JwtAuthenticationFilter(validates the 15-minute HMAC256 access token and populates theSecurityContext). Login issues the access token plus a 14-day rotating opaque refresh token (SHA-256-hashed at rest, family reuse-detection). Passwords are Argon2id-hashed, with legacy BCrypt hashes transparently upgraded on login. Exponential account lockout kicks in after 5 failures. - Authorization happens at the controller method:
@PreAuthorizechecks permission-name authorities (e.g.hr.employees.create) resolved from the RBAC tables (users,rolewith hierarchy,permission,role_permission,user_role). The frontend's "active role" switcher is a UI lens only — the backend always authorizes against the union of the user's permissions. - The customer portal is a separate auth system: its own endpoints
(
/api/portal/auth/*), its own 24-hour JWT (PORTAL_CUSTOMER), no refresh rotation, stored client-side in a Zustand store rather than Redux.
5. Data layer
- Schema is owned by Flyway: 87 migration files, versions V1–V88
(V26 unused), creating 91 tables — RBAC, HR, payroll + versioned statutory
rate cohorts, recruitment, CRM, inventory, portal, billing, auth hardening.
out-of-order: trueandbaseline-on-migrate: trueaccommodate parallel feature branches (backend/entities-and-migrations.md). - Entity-relationship diagrams: erd.md.
- The demo stack layers extra seed migrations (
demo/seed/V20–V24) viaSPRING_FLYWAY_LOCATIONS(deployment/docker.md).
6. Demo data seeding
To make the system demoable immediately after docker compose up -d --build,
the backend ships a DemoDataSeeder (ApplicationRunner, runs after Flyway):
if no transactional data exists, it generates timesheets, overtime, leave
balances/requests, bonuses, and two payroll runs (one processed with real
payslips via the actual payroll engine, one pending). System Administrators
can reset/regenerate on demand from Settings → Demo Data
(/api/system-admin/demo-data/status|generate|reset, gated by
system.admin.demo.data.manage). Seeding failures log warnings rather than
failing startup.
7. Deployment shapes
| Shape | Compose file(s) | Notes |
|---|---|---|
| Local dev | docker-compose.yml | Ports 5173/8081/5434; Swagger on; ALTCHA off. |
| Demo sandbox | docker-compose.demo.yml | Isolated DB + rich seed; ports 5174/8082/5435; ALTCHA on. |
| Monitoring overlay | docker-compose.monitoring.yml | Adds the full metrics/logs/traces/alerting stack to any environment. |
| Production (per client) | docker-compose.client.template.yml + infra/traefik/ | No host ports; Traefik routes <slug>.<domain>; TLS at Cloudflare. See deployment/vps-guide.md. |
CI runs Playwright E2E, CodeQL, and gitleaks — there is no automated deploy pipeline; production deploys are script-driven (deployment/ci.md).
8. Companion projects in this repo
- Legacy JavaFX desktop app — root
pom.xml,mvnw,manifest.mf: build leftovers of the pre-web predecessor (itssrc/has been removed). Not built or shipped; history in the archive. - MotorPH Event-Driven Inventory System — a separate Spring Boot +
RabbitMQ microservices project (own compose, own
.env), the eventual replacement for the in-monolith warehouse module. Built independently in CI. - marketing/ — content system for the public marketing pages
(
marketing/seo/entities.mdis the canonical copy source).