One platform for commercial real estate in Sri Lanka.
CPLK is a multi-tenant SaaS that lets agencies, individual property owners and the CPLK platform team list, approve, promote, transact and monitor commercial property listings — backed by a Spring Boot 3 / Java 21 API, a Next.js 16 portal, Keycloak identity and PayHere payments.
At a glance
com.cplk.apiWhat CPLK does, in one paragraph
Real-estate agencies and individual property owners create an account, get scoped into their own tenant in Keycloak and the database, and publish commercial property listings across eight types (Shop Showroom Office Co-Working Hotel/Resort Warehouse Industrial Land). Listings flow through a state machine — draft → pending review → approved/published → sold/leased/archived — gated by platform staff (Property Approvers). Agencies buy subscription packages for ad slots and credits, and pay for per-property boosts through PayHere. Public visitors browse SEO pages, filter by city/type/transaction, save favourites and submit inquiries that become tenant-scoped leads. Every write is audited; every query is tenant-isolated by a Hibernate filter driven from the JWT.
The system, at a glance
A single API serves three audiences: the public SEO site, the authenticated portal for agency staff, and the platform admin surface for CPLK staff. Identity is delegated to Keycloak; tenancy is enforced at the persistence layer; payments flow through PayHere.
SEO pages"] PORTAL["Agency portal
authenticated"] ADMIN["CPLK staff
platform admin"] end subgraph EDGE["Edge"] direction TB NGINX["Nginx
TLS + HSTS"] WEBHOST["PM2 / Cloudflare Pages
Next.js standalone"] end subgraph APP["Application"] direction TB WEB["Next.js 16 Web
SSR · ISR · CSR"] API["Spring Boot 3 API
Java 21 · /api"] end KC["Keycloak
auth.cplk.org"] subgraph DATA["Data & messaging"] direction TB PG[("PostgreSQL 16
Flyway × 142")] R2[("Cloudflare R2
images · docs")] MQ["RabbitMQ
notifications"] REDIS[("Redis
SSE fan-out")] end subgraph EXT["External"] direction TB PAYHERE["PayHere
checkout · recurring"] SMTP["SMTP
transactional email"] end PUB --> NGINX PORTAL --> NGINX ADMIN --> NGINX NGINX --> WEBHOST WEBHOST --> WEB WEB -- REST --> API API --> KC API --> PG API --> R2 API --> MQ API --> REDIS API --> PAYHERE PAYHERE -. webhook .-> API API --> SMTP
agency_id from claims and pins it to a Hibernate filter for the duration of the request.Who uses it
SEO visitor
Browses listings, filters by city / type / transaction, reads blogs, submits inquiries. Pages are SSR with ISR; the sitemap covers 1,000+ filter combinations.
Agency staff
Super admin manages billing & team; admin handles listings & inquiries; agents create & edit listings. All queries scoped to one agency by Hibernate tenant filter.
CPLK staff
Super admins, property approvers, financial officers, blog editors and support agents. Selected roles cross tenants via @BypassTenantFilter.
Where to go next
Business & domain
Personas, property types, monetisation, the full property/article/payment lifecycle.
System architecture
C4 context & container views, request lifecycle, cross-cutting concerns.
Deep architecture
Sequence diagrams: auth + tenant, property publish, PayHere webhook, recurring charge.
Backend deep dive
28 packages, 52 entities, 36 controllers, state machines, ArchUnit, conventions.
Frontend deep dive
Next 16 App Router map, service layer, refresh-queue API client, Zustand + React Query.
Data & integrations
ERD, entity catalogue, Flyway timeline, PayHere / Keycloak / R2 / RabbitMQ / Redis.
Security & IAM
Keycloak, JWT, 11 roles, RBAC matrix, multi-tenancy enforcement, rate limit.
Deployment
Hetzner topology, Makefile targets, Nginx, systemd, PM2, env matrix.
Operations & runbooks
Testing pyramid, observability, known gotchas, incident playbooks.
Technology stack
| Layer | Technology | Notes |
|---|---|---|
| Language | Java 21 · TypeScript 5 | Backend on Java toolchain 21; Next.js on Node 20+. |
| Backend | Spring Boot 3.5, Spring Security 6, Spring Data JPA, MapStruct 1.5, Caffeine 3, Bucket4j 8, ShedLock 5 | Stateless OAuth2 resource server; constructor injection only. |
| Frontend | Next.js 16 (App Router), React 19, TypeScript, TailwindCSS, Shadcn UI, React Query, Zustand, Zod, react-hook-form | Public pages SSR/ISR; portal CSR with <ProtectedRoute>. |
| Database | PostgreSQL 16, Flyway 10 | JSONB columns via Hypersistence Utils. |
| Identity | Keycloak (cplk realm) | OIDC; realm role mapping; org per agency. |
| Storage | Cloudflare R2 (S3 API) · local fallback | Thumbnailator + WebP variants; EXIF stripped. |
| Messaging | RabbitMQ · Redis pub/sub for SSE | In-memory SSE fallback for single-node dev. |
| Payments | PayHere (Sri Lanka) | Hosted checkout, preapproval token, recurring charging API. |
| Testing | JUnit 5 · Testcontainers · ArchUnit · Playwright 1.57 | 1,247+ backend tests, 85 E2E specs. |
| Hosting | Hetzner VPS × 2 · Cloudflare Pages (prod web) | Nginx + systemd on API VPS; PM2 (dev web), Pages (prod web). |
Conventions that hold everything together
API contract
- All responses wrapped in
ApiResponse<T>; paginated endpoints useApiResponse<PagedResponse<T>>. - Internal / portal routes use
/api/properties/{uuid}; SEO routes use/api/properties/slug/{slug}. - Default page size 20, sorted by
createdAtdescending unless overridden. - Validation via
@Valid+ Bean Validation; mapped toApiErrorby the global handler.
Multi-tenancy
- Every business entity extends
TenantAwareEntitywith anagency_idcolumn. TenantFilterreadsagency_idfrom the JWT and stores it on a ThreadLocal.TenantFilterAspectwraps every repository call and enables the HibernatetenantFilter.- Methods marked
@BypassTenantFilterskip the filter (SUPER_ADMIN cross-tenant queries, system jobs).
Audit & financials
AuditServicelogs every write (72 call-sites across 20 services) — async, REQUIRES_NEW.FinancialAuditLogis a separate immutable ledger for money & credit movements.- PayHere webhook idempotency is enforced by a UNIQUE constraint on
payments.order_id.
Testing & ArchUnit
- TDD mandated for backend; tests precede implementation.
- 16 ArchUnit rules enforce layering, naming, entity-non-leakage, no field injection, ApiResponse wrapping.
- Playwright E2E split into 10 role-projects so each spec runs under exactly the auth state it needs.