A multi-tenant credit risk analysis platform for SMEs, built on a GraphRAG pipeline (Neo4j + LangChain4j) with a Spring Boot backend and an Angular 17 frontend.
Upload a financial PDF → entities are extracted by an LLM → a relationship graph is materialized in Neo4j → ask natural-language questions and get a risk score backed by both graph evidence and computed financial ratios.
| Layer | Stack |
|---|---|
| Backend | Java 21, Spring Boot 3.3, Spring Data Neo4j, Spring Security OAuth2 RS, Lombok |
| AI | LangChain4j 0.31 + OpenAI GPT-4o (entity extraction & GraphRAG) |
| PDF parsing | Apache PDFBox 3.0 |
| Graph DB | Neo4j 5 (APOC plugin) |
| Auth | Keycloak 24 (realm-per-organization, PKCE public client) |
| Frontend | Angular 17 standalone components + signals, Tailwind, ng2-charts, ngx-graph |
| DevOps | Docker Compose (local), per-service Dockerfile |
You only need Docker and Docker Compose installed locally.
docker --version # Docker 20+ recommended
docker compose version # v2+(For development outside containers, you'd also want Java 21, Maven 3.9+, and Node 20+.)
From the repository root:
# 1. Configure environment
cp .env.example .env
# Then edit .env and set OPENAI_API_KEY=sk-...
# (without it, uploads still work but entity extraction returns empty results)
# 2. Bring up the infrastructure (Neo4j, Postgres, Keycloak)
docker compose up -d neo4j postgres keycloak
# 3. Build and start the application services
docker compose up -d --build backend frontend
# 4. Watch logs (optional)
docker compose logs -f backend frontendFirst boot takes 1–3 minutes (Keycloak needs to import the realm, the backend waits for Neo4j health). Subsequent boots are seconds.
| Service | URL |
|---|---|
| Frontend (the app) | http://localhost:4200 |
| Backend health | http://localhost:8080/actuator/health |
| Keycloak admin console | http://localhost:8180/admin |
| Keycloak realm | http://localhost:8180/realms/riskgraph |
| Neo4j browser | http://localhost:7474 |
- Open http://localhost:4200 — the app redirects to Keycloak.
- Sign in with the seeded user:
- Username:
admin@riskgraph.com - Password:
admin123
- Username:
- You'll land on the dashboard with
ADMIN,ANALYST, andVIEWERrealm roles.
- URL: http://localhost:8180/admin
- Username:
admin - Password:
admin - Switch the top-left realm dropdown from
master→riskgraphto manage app users.
- URL: http://localhost:7474
- Connect to
bolt://localhost:7687 - User:
neo4j— Password:riskgraph123(matches.env.example)
Sample PDFs are pre-generated under sample-pdfs/:
| File | Profile | Expected risk level |
|---|---|---|
01-acme-industries.pdf |
Healthy SME (manufacturing) | LOW – MEDIUM |
02-northstar-logistics.pdf |
Distressed SME (logistics) | HIGH – CRITICAL |
03-pixelwave-tech.pdf |
Mid-market SaaS | MEDIUM |
Workflow in the UI:
- Go to Upload, set a company name and sector, drop in one of the PDFs.
- Wait ~10–20 s for status to flip from
PROCESSING→DONE. - From the dashboard, click into the company → run Analyze with a question like "What is the credit risk over the next 12 months?"
- Open Graph view to see the extracted suppliers, owners, and documents as nodes connected to the company.
To regenerate or extend the sample PDFs:
cd sample-pdfs
pip install reportlab
python3 generate.pyAll secrets and host overrides live in .env:
| Variable | Default | Notes |
|---|---|---|
NEO4J_URI |
bolt://neo4j:7687 |
Service-name DNS inside compose |
NEO4J_USER |
neo4j |
|
NEO4J_PASSWORD |
riskgraph123 |
Used both by Neo4j and the backend |
KEYCLOAK_URL |
http://keycloak:8180 |
Inside compose; backend uses this |
OPENAI_API_KEY |
(empty) | Required for real entity extraction |
riskgraph/
├── docker-compose.yml
├── .env.example
├── keycloak/
│ └── realm-export.json # imported on first Keycloak boot
├── backend/
│ ├── pom.xml
│ ├── Dockerfile # multi-stage: maven build + JRE runtime
│ └── src/main/java/com/riskgraph/
│ ├── RiskGraphApplication.java
│ ├── config/ # Security, Neo4j, LangChain4j wiring
│ ├── domain/ # @Node entities + @RelationshipProperties
│ ├── repository/ # Spring Data Neo4j repos
│ ├── security/ # TenantContext + TenantFilter
│ ├── service/ # Ingestion, Extraction, GraphBuilder, GraphRAG, Scoring
│ ├── controller/ # REST endpoints
│ └── dto/
└── frontend/
├── package.json
├── Dockerfile # node build → nginx
├── nginx.conf # serves SPA, proxies /api/ to backend
└── src/app/
├── app.config.ts
├── app.routes.ts
├── core/
│ ├── auth/keycloak.service.ts
│ ├── interceptors/auth.interceptor.ts
│ ├── guards/auth.guard.ts
│ ├── services/api.service.ts
│ └── models/api.models.ts
├── features/
│ ├── dashboard/
│ ├── upload/
│ ├── analysis/
│ └── graph-view/
└── shared/components/ # sidebar, header, risk-badge
| Method | Path | Roles | Purpose |
|---|---|---|---|
| POST | /api/documents/upload |
ADMIN, ANALYST | Upload a PDF (multipart) |
| GET | /api/documents/{id}/status |
ADMIN, ANALYST, VIEWER | Polling for ingestion status |
| GET | /api/documents |
ADMIN, ANALYST, VIEWER | List documents (current tenant) |
| POST | /api/analysis/query |
ADMIN, ANALYST | GraphRAG risk analysis |
| GET | /api/analysis/{companyId}/score |
ADMIN, ANALYST, VIEWER | Latest computed risk score |
| GET | /api/companies |
ADMIN, ANALYST, VIEWER | List companies (current tenant) |
| GET | /api/companies/{id} |
ADMIN, ANALYST, VIEWER | Company details |
| GET | /api/companies/{id}/graph |
ADMIN, ANALYST, VIEWER | Nodes + edges for visualization |
| GET | /api/companies/dashboard/summary |
ADMIN, ANALYST, VIEWER | KPI cards + charts data |
All endpoints require a valid Keycloak JWT and are filtered by the organization
claim in the token (multi-tenant isolation).
docker compose down # stop, keep data
docker compose down -v # stop and wipe Neo4j + Postgres volumesFrontend redirects to Keycloak then errors out.
Make sure Keycloak finished importing the realm:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8180/realms/riskgraph
should return 200. On the first boot it takes 30–60 s.
POST /api/analysis/query returns 500 or a low-confidence fallback answer.
You probably haven't set OPENAI_API_KEY in .env. The backend logs
GraphRAG LLM call failed and returns a fallback response instead of crashing.
Backend can't connect to Neo4j on first boot.
The compose health check waits for Neo4j to be healthy before starting the backend.
If you used docker compose up backend before Neo4j was ready, restart with
docker compose restart backend.
Port already in use.
Default ports are 4200 (frontend), 8080 (backend), 8180 (Keycloak), 7474/7687 (Neo4j).
Either stop the conflicting service or override the host port in docker-compose.yml.