All work

AI · Full-Stack · Case study

FinGraph AI

Banking assistant over a Neo4j relationship graph. pgvector semantic search finds the relevant customers, loans and chats, Neo4j enriches every match with its relationships, and Groq writes the answer. JWT + bcrypt authentication guards the admin and audit APIs.

Team
Solo
Timeline
January 2026
Stack
Node.js · Express · React · Neo4j · Supabase pgvector · Hugging Face · Groq · JWT
How it worksA question, end to end
  1. 01

    Chat UI

    frontend/src/api/chat.api.js

  2. 02

    Chat route

    api/routes/chat.routes.js

  3. 03

    Customer profile

    services/chat.service.js

  4. 04

    Embed question

    services/embedding.service.js

  5. 05

    Vector search

    db/supabase/driver.js

  6. 06

    Graph enrichment

    services/vector.service.js

  7. 07

    Groq answer

    services/llm.service.js

DataNeo4jSupabase pgvector

Situation

Banking questions are about relationships: which customer holds which loan, what they asked before, how their records connect. Plain vector search finds similar text but loses those links, so answers come back plausible and unanchored.

Task

Build an assistant that retrieves by meaning and then grounds each result in the graph of customers, loans and conversations.

Action

I modelled users, loans and chats as a Neo4j graph and mirrored their text into Supabase pgvector with MiniLM embeddings from Hugging Face. A question is embedded, matched by similarity above a threshold, and every match is enriched with a Cypher query for its relationships (a loan’s borrower, a user’s loans and chats) before Groq writes the answer. Authentication is JWT with bcrypt-hashed passwords stored in Neo4j, and the admin and audit APIs require the ADMIN role.

backend/src/services/vector.service.jsjavascript
// Embed the question, find similar items in pgvector, then enrich each hit from Neo4j.
async function semanticSearch(query, contentType = null, limit = 5) {
  const queryEmbedding = await generateEmbedding(query);

  const vectorResults = await searchSimilar({
    queryEmbedding,
    contentType,
    limit,
    threshold: 0.3,
  });

  if (vectorResults.length === 0) {
    return [];
  }

  return enrichWithGraphData(vectorResults);
}

Result

A banking assistant whose answers carry the relationships behind them, not just the closest matching text.

Graph + Vector

hybrid retrieval

Neo4j

relationship enrichment

JWT + bcrypt

authentication

Looking back

What I would do differently

  • Put JWT middleware on /api/chat and take the role from the verified token, not the request body.
  • Mount the audit writer, so every question is actually logged.