The problem
For a smaller contact-center operation, customer context is scattered across calls, chats, and CRM records with no connective tissue — the same customer, agent, and issue appear everywhere but are never linked, so relationship-level questions can't be answered.
Row-and-column analytics flatten those relationships; understanding how interactions, identities, and outcomes connect needs a graph, not another table.
The approach
Interactions are ingested and modeled as a versioned knowledge graph — a first-class ontology of nodes and edges over customers, interactions, identities, and channels — stored in Neo4j as the primary system of record.
A background worker handles ingestion and enrichment off the request path via a job queue, while a typed graph client exposes named, tested Cypher queries so the application layer never hand-writes ad-hoc graph traversals.
The whole system is multi-tenant by construction: an auth provider's Organizations map to Workspaces, and a relational store holds auth sync, billing, and audit state separately from the graph.
What I built
- Neo4j-backed customer-interaction knowledge graph with a versioned ontology package (node/edge definitions, constraints, seed data) as the schema source of truth
- Next.js dashboard and a Hono REST API, both authenticated with per-workspace JWTs, over a pnpm/Turborepo monorepo
- BullMQ/Redis ingestion worker plus a typed graph-query layer of named Cypher queries validated against a real Neo4j instance via testcontainers
- Postgres (Drizzle ORM) for auth-provider sync, billing, and audit log, kept separate from the graph store
- Dedicated packages for channel ingestion, CRM integration, and speech-to-text, so new interaction sources plug into the same graph
- Multi-tenant workspace model (auth Organizations = Workspaces) with every service targeting a Railway deployment path
- TypeScript
- Next.js 15
- Hono
- Neo4j 5.x
- PostgreSQL
- Drizzle ORM
- BullMQ
- Redis
- Clerk
- pnpm/Turborepo
- Vitest + Testcontainers
- Railway