A multi-tenant SaaS where a customer files a motor-insurance claim, documents are read by OCR, photos are checked for fraud, a rules engine scores the risk, an investigator is assigned, and the claim is approved or rejected β with strict tenant isolation and a complete audit trail.
Spring Boot 4 Β· Java 21 Β· PostgreSQL 17 Β· Next.js 16 Β· React 19 Β· Google Vision Β· Gemini RAG
- π― The problem & the idea
- β¨ What it does
- ποΈ Architecture
- π§± Tech stack
- π How a claim flows (example)
- π Security model
- π€ The intelligence layer
- π₯ Roles & demo logins
- π Quick start
- π Project structure
- π§ͺ Testing
- πΊοΈ Roadmap
- π¨βπ» Author
Insurance claims live in a permanent tension between speed and caution:
- π The policyholder wants their genuine claim settled quickly and fairly.
- π’ The insurer wants to pay every valid claim, catch the fraudulent ones, and keep an audit trail for the regulator.
Lean too far toward speed and you leak money to fraud. Lean too far toward caution and you punish honest customers and bury investigators in paperwork.
ClaimLens resolves this tension with software: read the documents automatically, surface only the claims that actually look suspicious, and record why every decision was made β so the same team handles far more volume without cutting corners. A human still owns every real decision; the platform just does the mechanical part.
|
π’ Multi-tenant by design Every insurer is a fully isolated tenant β enforced in the database, not by convention. Two tenants can never see each other's data. πΈ OCR document intake RC books, FIRs, repair estimates and invoices are read automatically (Google Cloud Vision), with registration & policy numbers extracted. π΅οΈ Image fraud forensics Perceptual-hash duplicate detection, ORB similarity, three-state EXIF checks, and a synthetic-image signal β catches reused and tampered damage photos. βοΈ Explainable fraud engine A config-driven rules engine scores every claim and shows exactly why β because an automated decision must be auditable. |
π€ AI policy intelligence (RAG) Ask coverage questions in plain language and get answers grounded in the actual policy wording, with citations back to the clauses. π Customer self-service portal Policyholders file, upload, submit and track their own claims β with ownership scoping below the tenant, so one customer can never see another's claim. π Platform console A cross-tenant operator view: platform-wide analytics, tenant lifecycle, and impersonation. π Full lifecycle & audit Three-step intake β auto-assignment β investigation β decision, with document versioning and an append-only audit trail. Two-way information requests: an investigator asks for a document, the customer uploads it from the portal, and the claim re-opens and reprocesses itself. π Self-service credentials New users are invited, not handed a password β a single-use, expiring link lets them set their own, so an admin never knows anyone else's credential. Same machinery powers password reset. π§ Rich claim reports Every claim email is branded HTML with an attached PDF report β key stats, the incident description, the decision and who made it, and the customer's uploaded photos embedded in the PDF. |
flowchart TD
User([π€ Customer / Staff]) -->|HTTPS| FE[π¨ Frontend<br/>Next.js 16 Β· React 19]
FE -->|REST /api/v1| BE[βοΈ Backend<br/>Spring Boot 4 Β· Java 21]
BE --> DB[(π PostgreSQL 17<br/>@TenantId isolation)]
BE -->|documents| OCR[π Google Vision OCR]
BE -->|images| AN[πΌοΈ Analysis Service<br/>Python Β· OpenCV]
BE -->|policy Q&A| RAG[π€ Gemini RAG<br/>+ pgvector optional]
BE -->|files| R2[(ποΈ Object storage<br/>R2 / S3)]
BE -->|notifications| MAIL[π§ Resend email]
subgraph Pipeline [β±οΈ Background pipeline]
OCR --> FRAUD[βοΈ Fraud engine]
AN --> FRAUD
FRAUD --> ASSIGN[π₯ Auto-assignment]
end
Key design choices
- π Tenancy is a database concern. Every tenant-scoped entity carries a Hibernate
@TenantIddiscriminator that's appended to every query β including load-by-id β and set automatically on insert. You can't forget it. - π Swappable clients everywhere. OCR, image analysis, embeddings, chat, email and storage each sit behind an interface with a no-op/offline default and a real implementation toggled by config β so tests and offline runs stay green, and real integrations slot in without code changes.
- π§© Best-effort by default. A document that fails, a service that's down, an AI key that's throttled β none of it hangs a claim. The pipeline settles on terminal state (succeeded or retries-exhausted), and AI degrades to an offline answer.
- π Explainability is a feature. Every fraud score keeps its per-rule breakdown; every RAG answer keeps its citations; every state change is audited.
| Layer | Technology |
|---|---|
| Backend | Java 21, Spring Boot 4, Spring Security (JWT), Spring Data JPA / Hibernate 7 |
| Database | PostgreSQL 17, HikariCP pool, Flyway migrations, optional pgvector (HNSW) |
| Cache | Spring cache abstraction β Caffeine (dev) / Redis Β· Upstash (prod), same code |
| Frontend | Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS 4, shadcn/ui, Redux Toolkit, axios |
| OCR | Google Cloud Vision (swappable with Tesseract) |
| Image analysis | Python Β· FastAPI Β· OpenCV Β· imagehash Β· Pillow |
| AI / RAG | Google Gemini (embeddings + chat), in-Java cosine or pgvector retrieval |
| Storage / Email | Cloudflare R2 (S3-compatible) Β· Resend |
| Infra | Docker (multi-stage) Β· Render Β· Vercel Β· Neon Β· Upstash |
Here's the end-to-end journey of a real claim π
1. π Rahul (customer) logs into the portal and files a claim on his policy DEMO-POL-1.
POST /api/v1/portal/claims β status: DRAFT
2. π He uploads a photo of the damaged windshield.
POST /api/v1/portal/claims/{id}/documents
3. β
He submits. Hard validations run (policy active on the loss date? vehicle covered?
claimant = policyholder?).
POST /api/v1/portal/claims/{id}/submit β status: AWAITING_ANALYSIS
4. βοΈ In the background:
π OCR reads "MH12AB1234" and the policy number off the document.
πΌοΈ Image analysis fingerprints the photo (perceptual hash) + checks EXIF.
βοΈ The fraud engine scores the claim and explains each rule β risk: LOW.
β status: AWAITING_ASSIGNMENT
5. π΅οΈ The Investigation Manager assigns it to an investigator.
POST /api/v1/claims/{id}/assign β status: UNDER_INVESTIGATION
6. π The investigator reviews the evidence and approves.
POST /api/v1/claims/{id}/decision β status: APPROVED
7. π§ Rahul gets an email: "Your claim has been approved." β and the whole path is audited.
π‘ A
TENANT_ADMINsees it all; theCUSTOMERonly ever sees their own claim; a rival tenant sees a 404. That boundary is enforced in the database and tested as a hard gate.
Two independent boundaries, both enforced server-side:
-
π’ Tenant isolation β Hibernate
@TenantIdscopes every query to the caller's tenant. A cross-tenant read isn't a403(which would confirm the record exists) β it's a404. Proven by a dedicated isolation test suite. -
π Ownership (customer portal) β because two customers share a tenant,
@TenantIdalone doesn't separate them. The portal adds a second gate: every read/write is scoped to the authenticated customer viafindByIdAndCustomerId(...), with the customer id coming from the JWT β never the request body.
Role-based access control β permissions are checked with @PreAuthorize on the service layer (not the UI). The JWT carries only a role id, never a permissions array, so a signed token can never hold stale authority β permissions are resolved server-side from the database. That resolution runs on every authenticated request, so it's cached (Caffeine in dev, Redis in prod) with a short TTL; roleβpermission mappings are migration-managed, so a change is a redeploy, which clears the cache. The frontend hides buttons a role can't use; the backend enforces the rule regardless.
Brute-force protection β sign-in is rate limited per client IP, and the cost-bearing endpoints (AI answers, document uploads) per user. Backed by Redis in production so the limit holds across instances, and it fails open: if the limiter is unreachable the request is allowed, because a throttle outage must never become a login outage.
// Real enforcement lives here, not in the client:
@PreAuthorize("hasAuthority('CLAIM_DECIDE')")
public ClaimResponse decide(Long claimId, ClaimDecisionRequest request) { β¦ }π OCR β documents into data. Vision reads the pixels back into text; permissive regexes then extract registration numbers, policy numbers and amounts. OCR output is a signal, never ground truth β it's cross-checked against the policy on record.
πΌοΈ Image forensics β cheap signals, honest limits.
- Perceptual hashing catches a damage photo reused across claims (the #1 motor-fraud pattern).
- ORB feature matching catches lightly-edited reuse that hashing misses.
- EXIF is three-state: only a genuine inconsistency (capture time before the incident) counts β missing metadata is never suspicion (social apps strip it).
- Synthetic-image detection is always a soft signal, never an auto-reject.
π§ RAG β answers from the policy itself. "Is windshield damage covered?" is answered from this policy's wording, with citations β and scoped to the claim's pinned version, so the answer reflects the terms the customer actually contracted under. Retrieval runs on in-Java cosine over JSON-stored embeddings by default (works on any Postgres), or pgvector + HNSW when enabled. If the AI provider is unavailable, it degrades gracefully to an offline extractive answer instead of failing.
Eight one-click demo accounts, each with a real permission set (password Password123!, sandbox only):
| Role | What they can do |
|---|---|
| π’ Platform Admin | Everything across every tenant; onboard/suspend tenants; impersonate |
| π Tenant Admin | Full workspace β org, products, policies, customers, claims, users, rulesets |
| π΅οΈ Investigation Manager | Read claims, assign to investigators, read fraud scores |
| π Investigator | Read claims, add notes, decide (approve/reject) |
| π Customer | File, upload, submit and track their own claims (portal) |
| π Customer Support | Create & submit claims on behalf of customers |
| π§βπΌ Employee (Adjuster) | Read policies, ask coverage questions |
| π Auditor | Analytics, audit trails, read-only org |
π The nav, buttons and pages a user sees are driven entirely by their server-resolved permissions.
# 1. Backend β http://localhost:8080
cd backend && cp .env.example .env # set DATABASE_PASSWORD + JWT_SECRET
./mvnw spring-boot:run # auto-loads .env, Flyway migrates + seeds demo data
# 2. Frontend β http://localhost:3000
cd frontend && npm install && npm run devThen open http://localhost:3000 and click a demo login. π
π Full step-by-step guide (DB, integrations, tests, troubleshooting): setup.md π Deploy to the cloud (Render + Vercel + Neon + R2 + Resend + Vision + cron): deploy.md
Claim-Lens/
βββ backend/ βοΈ Spring Boot API β auth, tenancy, claims, fraud, RAG, portal, platform
β βββ src/main/java/β¦/claimlens/
β β βββ tenancy/ π @TenantId multi-tenancy
β β βββ security/ π JWT + RBAC
β β βββ claim/ π claim lifecycle
β β βββ processing/ β±οΈ OCR / analysis / fraud orchestration
β β βββ coverage/ π€ RAG (embeddings, chat, pgvector)
β β βββ portal/ π customer self-service
β β βββ platform/ π cross-tenant console
β βββ src/main/resources/db/migration/ ποΈ Flyway (V1β¦V26)
βββ frontend/ π¨ Next.js app β staff workspace, portal, platform, landing, blog, roadmap
βββ analysis-service/ πΌοΈ Python/FastAPI image-fraud service (OpenCV)
βββ ocr-service/ π Python/FastAPI OCR service (Tesseract/Vision) β alt to in-JVM Vision
βββ docs/ π design docs
βββ setup.md π οΈ local setup
βββ deploy.md βοΈ deployment
cd backend && ./mvnw test # 92 integration & unit testsThe suite runs against real PostgreSQL (not H2 β the app uses JSONB, partial indexes and TIMESTAMPTZ), with Flyway re-validating every migration on each run. Highlights:
- π Tenant isolation β as tenant A, reading tenant B's record returns 404, seeded via raw JDBC so the reader is what's proven filtered.
- π Ownership isolation β customer A gets 404 for customer B's claim in the same tenant.
- π RBAC β a revoked permission is denied on the next request (the cache is disabled under the test profile, so this proves the live path).
- βοΈ Full pipeline β draft β submit β OCR β analysis β fraud β assign β decide.
- π€ RAG β retrieval is tenant-scoped and citations point at the right clause.
- π¦ Rate limiting β repeated sign-ins get a 429, while ordinary reads are never throttled.
- π€ Onboarding β a customer login must be linked to a policyholder, and an invited account can't sign in until it sets a password.
- β‘ Cache resilience β a cache failure degrades to a database read instead of failing the request.
A 240-check cross-role sweep additionally verifies every role can do exactly what it should β and nothing it shouldn't.
What's shipped and what's next lives on the in-app /roadmap page. In short:
- β Shipped: multi-tenancy, full claim lifecycle, Google Vision OCR, image forensics, explainable fraud engine, AI policy Q&A, customer portal, platform console, document versioning, email.
- π Next: cloud hardening (rate limiting, metrics, structured logs), pgvector at scale, more claim types (health/property), real-time notifications, outbox event worker, claimant mobile app, richer analytics/export.
Yogesh Chauhan π yogeshchauhan.dev Β· π GitHub Β· πΌ LinkedIn Β· βοΈ chauhanyogesh950@gmail.com
Built to show that "fast" and "careful" don't have to be a trade-off. π‘οΈ