Architecture
An overview of how Ranlanka CRM is structured, for anyone extending or
operating the app. For line-level internal notes, see KNOWLEDGE_BASE.md in
the project root — this page is the readable summary of that file.
Repo layout
Two independent repos, developed side by side but deployed separately:
- Spring Boot 3.5 · Java 23 backend
- React 19 · Vite frontend
Each has its own git history, CI/CD, and pre-commit hooks — there is no shared
root repo or shared package.json.
Backend
A modular monolith: every business domain is its own module under
modules/* (staff, customer, booking, finance, accounting,
quotation, checklist, dashboard, reference, auth), each following the
same layering:
Controller → Service → Repository → EntityCross-cutting concerns (security, email, scheduling, file storage, logging)
live in shared/* and are used by every module.
Conventions
- IDs are UUIDs, generated by Postgres (
gen_random_uuid()), never by the application. - Soft deletes — most entities carry a
deleted_atcolumn and a@SQLRestriction("deleted_at IS NULL")annotation, so deleted rows are filtered out of every query automatically. - Schema changes are migration-only — Hibernate’s DDL auto-generation is
disabled (
validatemode). Every schema change is a newV<n>__description.sqlfile underdb/migration/, applied by Flyway on startup. - RBAC is database-backed, not a Java enum — permission strings (e.g.
VISA_CREATE,INVOICE_CANCEL) live in theroles/role_permissionstables and are checked via@PreAuthorize("hasAuthority('X')"). - Uniform API responses — every controller returns
ApiResponse<T>.
Scheduled jobs
Two @Scheduled cron jobs run in-process (no external scheduler):
| Job | Schedule | Purpose |
|---|---|---|
ReminderSchedulerService | 0 0 8 * * * (08:00 daily) | Visa-expiry, flight-today, and recurring-bill reminder emails |
QuoteCleanupService | 0 0 3 * * * (03:00 daily) | Soft-deletes quotes past their 3-day expiry |
Both are stateless across restarts — the cron expression is fixed in code and all actual state (dedup logs, per-job settings) lives in Postgres, so a redeploy never loses or duplicates a scheduled run.
PDF & email rendering
No templating engine — HTML for both PDFs and emails uses plain
{{PLACEHOLDER}} string substitution on .html files. PDFs are rendered via
openhtmltopdf, which does not support CSS flexbox — layouts in the three
PDF templates (invoice, quote, receipt) use block/table layout instead.
Frontend
React 19 + Vite, Redux Toolkit for state, Tailwind CSS v4 for styling
(@theme custom properties, with a :root.dark override block for dark
mode). Routing via react-router-dom.
Deployment
- Backend ships as a Docker image (multi-stage build, non-root user),
published to Docker Hub via GitHub Actions on push to
main. - Database is Supabase-hosted Postgres — no local Postgres container in the deploy compose file.
- Hosting is a single Oracle Cloud “Always Free” instance, deployed via a manual-trigger GitHub Actions workflow.
- Logging ships to Grafana Cloud’s hosted Loki via an
alloyshipper, toggled by a Compose profile.
See Security for the auth model and permission system.