Generated
Commercial Property · Sri Lanka

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.

Stack Java 21 · Next.js 16 Database PostgreSQL 16 Identity Keycloak Payments PayHere (LK) Hosting Hetzner + Cloudflare

At a glance

Backend domains
28
feature packages under com.cplk.api
JPA entities
52
all extend BaseEntity / TenantAwareEntity
REST controllers
36
ApiResponse<T> envelope, PagedResponse for lists
Services
69
constructor-injected, ArchUnit-enforced
Flyway migrations
142
never modified once applied
Backend tests
1,247+
94.9% coverage · 80% gate
ArchUnit rules
16
layering, naming, contracts
E2E specs
85
Playwright · 10 role projects

What 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.

flowchart LR subgraph CLIENT["Clients"] direction TB PUB["Public visitors
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
High-level system map. Every authenticated request carries a Keycloak-issued JWT; the API extracts agency_id from claims and pins it to a Hibernate filter for the duration of the request.

Who uses it

Public

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.

Tenant

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.

Platform

CPLK staff

Super admins, property approvers, financial officers, blog editors and support agents. Selected roles cross tenants via @BypassTenantFilter.

Technology stack

LayerTechnologyNotes
LanguageJava 21 · TypeScript 5Backend on Java toolchain 21; Next.js on Node 20+.
BackendSpring Boot 3.5, Spring Security 6, Spring Data JPA, MapStruct 1.5, Caffeine 3, Bucket4j 8, ShedLock 5Stateless OAuth2 resource server; constructor injection only.
FrontendNext.js 16 (App Router), React 19, TypeScript, TailwindCSS, Shadcn UI, React Query, Zustand, Zod, react-hook-formPublic pages SSR/ISR; portal CSR with <ProtectedRoute>.
DatabasePostgreSQL 16, Flyway 10JSONB columns via Hypersistence Utils.
IdentityKeycloak (cplk realm)OIDC; realm role mapping; org per agency.
StorageCloudflare R2 (S3 API) · local fallbackThumbnailator + WebP variants; EXIF stripped.
MessagingRabbitMQ · Redis pub/sub for SSEIn-memory SSE fallback for single-node dev.
PaymentsPayHere (Sri Lanka)Hosted checkout, preapproval token, recurring charging API.
TestingJUnit 5 · Testcontainers · ArchUnit · Playwright 1.571,247+ backend tests, 85 E2E specs.
HostingHetzner 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 use ApiResponse<PagedResponse<T>>.
  • Internal / portal routes use /api/properties/{uuid}; SEO routes use /api/properties/slug/{slug}.
  • Default page size 20, sorted by createdAt descending unless overridden.
  • Validation via @Valid + Bean Validation; mapped to ApiError by the global handler.

Multi-tenancy

  • Every business entity extends TenantAwareEntity with an agency_id column.
  • TenantFilter reads agency_id from the JWT and stores it on a ThreadLocal.
  • TenantFilterAspect wraps every repository call and enables the Hibernate tenantFilter.
  • Methods marked @BypassTenantFilter skip the filter (SUPER_ADMIN cross-tenant queries, system jobs).

Audit & financials

  • AuditService logs every write (72 call-sites across 20 services) — async, REQUIRES_NEW.
  • FinancialAuditLog is 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.