Skip to main content

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

ServiceImageHost portWhy it exists
motorph_payroll_dbpostgres:165434 → 5432Primary datastore. Schema owned entirely by Flyway (V1–V88); Hibernate runs ddl-auto: validate and never generates schema.
motorph_payroll_backendbackend/Dockerfile8081 → 8080The REST API and the only process with DB access. Also serves the STOMP WebSocket (/ws), Swagger UI, and Prometheus metrics.
motorph_payroll_frontendfrontend/Dockerfile5173 → 80nginx 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_pgadmindpage/pgadmin4:9.16127.0.0.1:5050 → 80Development 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_mailpitaxllent/mailpit:v1.30127.0.0.1:8025 → 8025Development 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.yml127.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):

  1. ERP app (authenticated) — HR, payroll, recruitment/ATS, CRM, inventory, self-service, billing, admin.
  2. Public marketing site — home/pricing/FAQ plus public careers pages that feed the recruitment module.
  3. 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 in api/ (~60 modules). frontend/state.md explains why state is split three ways.
  • api/client.ts builds the axios instance. VITE_API_URL defaults 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 @PreAuthorize constants). 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 the SecurityContext). 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: @PreAuthorize checks permission-name authorities (e.g. hr.employees.create) resolved from the RBAC tables (users, role with 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: true and baseline-on-migrate: true accommodate parallel feature branches (backend/entities-and-migrations.md).
  • Entity-relationship diagrams: erd.md.
  • The demo stack layers extra seed migrations (demo/seed/V20–V24) via SPRING_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

ShapeCompose file(s)Notes
Local devdocker-compose.ymlPorts 5173/8081/5434; Swagger on; ALTCHA off.
Demo sandboxdocker-compose.demo.ymlIsolated DB + rich seed; ports 5174/8082/5435; ALTCHA on.
Monitoring overlaydocker-compose.monitoring.ymlAdds 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 (its src/ 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.md is the canonical copy source).