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>. - Rate Limiting — API endpoints are protected by
RateLimitingFilter, enforcing a 100 req/min sliding window and returning an HTTP 429 response on limit exceeded. This works alongside Nginxlimit_req_zone. The application usesserver.forward-headers-strategy=frameworkto properly resolve client IPs. - Statistics Matrix Endpoints — Dedicated backend endpoints compute multi-attribute breakdowns:
/api/v1/visas/stats-matrix,/api/v1/flights/stats-matrix, and/api/v1/tours/stats-matrix. - Cloudflare R2 Object Storage (
R2FileStorageServiceImpl) — All uploaded customer files, visas, tickets, and identity documents are stored in Cloudflare R2 S3-compatible buckets (ranlanka-crm/ranlanka-crm-dev). Files use deterministic hierarchical keys (customers/{id}/...) and are served via presigned URLs with forcedContent-Disposition: attachmentheaders.
Scheduled jobs
Three @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 |
DailyDigestScheduler | 0 0 19 * * * (19:00 Asia/Colombo) | Daily summary email of new visas, flights, tours, and financial totals with CSV attachment sent to owners/managers |
All are stateless across restarts — the cron expressions are fixed in code and
all actual state (dedup logs, per-job settings) lives in Postgres (reminder_send_log), 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.