After a Nigerian School Reprinted 2,000 Report Cards, They Built a Memory with a Graph Database

Ednovax built a FalkorDB graph memory for its support team after a costly report card reprint.
Ednovax provides school management systems in Nigeria. After failing to connect a school's new report card format request with its separate batch-printing code path, it reprinted 2,000+ cards at its own cost of over 250,000 naira. Realizing the problem was memory, not code, the team built Loopkeeper on FalkorDB — a graph memory for its WhatsApp support team with one graph per school.
After a Nigerian School Reprinted 2,000 Report Cards, They Built a Memory with a Graph Database
A company that develops management systems for Nigerian schools had to pay out of pocket to reprint over 2,000 report cards because of a communication breakdown. Instead of writing a post-mortem or adding more process, they built a graph database for their support team — turning every conversation into a queryable memory.
A forgetting that cost 250,000 naira
Ednovax provides school management systems for schools in Edo and Delta States, Nigeria. After the Ministry of Education changed the report card format, multiple schools requested updates over WhatsApp. The team updated the grades page but missed a key fact: one school, for technical reasons, used a separate feature for batch printing, and the new format had never covered that path.
No one connected "this school needs the new format" with "this school's printing goes through a different code path." The school printed over 2,000 copies without a proof first, and all were wasted. Ednovax covered the reprint cost — over 250,000 naira — plus a hard-won measure of trust.
The author put it plainly in the original post: "The code wasn't the problem. Memory was."

The support team lives in WhatsApp
This is not a typical customer support scenario. Ednovax's school clients never file tickets; they don't use Jira or Zendesk, and they don't plan to. Principals message developers directly. Exam officers drop a feature request between a "good morning," a prayer, and three birthday wishes in a group chat. Requests might arrive as late-night screenshots with no accompanying text.
This creates three persistent problems: who asked for what and when is scattered across individual chats and groups; promises made are quickly forgotten; and each school's deployment method, customizations, and exceptions live only in a few people's heads.
The report card incident was the compound effect of all three: the request was in the chat history, the exception was in someone's memory, and there was no link between them. What they needed wasn't a new inbox but a memory that could read existing chats and remember key facts and their relationships.
Why memory must be a graph
The real question to ask is: which schools use the report card feature? How is each school deployed? What other code depends on the files that need changing?
This isn't a table lookup; it's a traversal: from a feature to the schools using it, to each school's deployment method, to the code files implementing the feature, to the dependencies importing those files, to the owners of the code. Support knowledge is inherently relational — a message is evidence of a problem, a problem affects a feature, a feature is implemented by code, a developer owns the code, and a school deploys from its own repository.
In a relational table, every hop is a join; in a graph, relationships are the data. The team ultimately chose FalkorDB for four reasons: Cypher queries turn each question into a readable pattern; a vector index on the same nodes lets "grades won't save" find a ticket titled "grades lost on submit" without a second database; built-in graph algorithms (connected components, PageRank, shortest path) for deduplication, ranking, and explaining connections; and multi-graph on a single instance gives each school its own isolated graph space.
Their principle was clear: not a guessing chatbot, but a structured memory — and a model only allowed to answer based on that memory.
Graph databases and the property graph model: A graph database uses nodes (entities) and edges (relationships) as its basic storage units, and edges themselves can carry properties. Thus relationships like "who owns which code" and "which message corresponds to which ticket" are first-class data just like entities. Query cost of traversing edges depends on path length, rather than doing a join for every table crossed as in a relational database. Cypher is a declarative query language for property graphs, describing the structure to match with patterns like (a)-[:REL]->(b), with readability close to natural language — which is why the article emphasizes "each question becomes a readable pattern."
Vector indexes and semantic search: A vector index maps text through an embedding model into high-dimensional vectors, so semantically similar content is closer in vector space, enabling retrieval where "grades won't save" matches "grades lost on submit" — different wording, similar meaning. Maintaining vector indexes on the same nodes in a graph database means semantic recall and relationship traversal can be completed in a single query, without introducing a second dedicated vector store.
How Loopkeeper works
The project is called Loopkeeper, launched as Ednovax Support, with the core goal that every issue a school opens never gets silently dropped. It attaches to the school's WhatsApp group, and teachers don't have to change a thing. Each incoming message goes through five steps.
Decide what counts as work. Most messages in school groups are greetings, prayers, and birthday wishes. A fast model triages each batch of messages, recognizing Nigerian English and light Pidgin (e.g., "e no gree save" means it won't save). Chit-chat is only counted, not stored; work messages are classified as bugs, questions, feature requests, promises, resolutions, updates, or decisions, while also judging the sender's sentiment as calm, concerned, or upset.
Check memory before writing. This is the team's proudest part. Before the model reads a message, FalkorDB hands it possibly relevant tickets: the sender's school's open tickets, the semantically closest (vector search), recently active ones, and those resolved within a few days. The model's first rule is "reuse before create." "Same here o" becomes a confirmation of an existing ticket; "It's working now, thank you" closes the correct ticket, and anything uncertain is flagged for human confirmation.
Connect the ticket to everything. A new ticket is linked to the source message, school, people involved, and product feature. The developer owning the most code for that feature gets auto-assigned. The system searches the real codebase semantically, reads the source of the closest files, and records which files and functions are most likely involved and why.
Remember promises. "I'll send the updated list before Thursday" becomes a follow-up with an owner and a deadline in Lagos time zone. When due, the system gives one gentle reminder in the group. Decisions ("mock exams moved to Friday") are recorded too.
Close the loop. One click sends the ticket to GitHub with the school's exact words, likely code, and owner. After the PR merges, the ticket becomes "possibly resolved," the school gets a plain-language notification, and the teacher's confirmation in the group is the final basis for closing.
Anyone on the team can ask in natural language: "What is Henson waiting on?" "Where is the grades bug in the code?" "Who promised what this week?" Every answer comes from a named Cypher query, cites the specific WhatsApp message, and highlights the nodes touched. What the graph doesn't know, it says it doesn't know.
Triage and small-model division of labor: Triage means quickly classifying and prioritizing each message before formal processing, deciding whether it's "work," what category it is, and its sentiment. Here they deliberately use a low-cost, low-latency model (Claude Haiku) for triage and information extraction, leaving the steps needing judgment and decisions to a more careful process — a typical layered design of "small model routes, large model judges."
Embedding model all-MiniLM-L6-v2: A lightweight sentence embedding model that outputs 384-dimensional vectors and runs locally on CPU without external API calls. It lets each WhatsApp message be vectorized at ingestion, at the cost of lower semantic precision than large embedding models — which is why the article uses graph queries to build a candidate union to compensate for vector recall alone.
The technical foundation: one graph per school and 182 named queries
The tech stack is deliberately lean: TypeScript server (Hono), React dashboard, Claude Haiku for triage, extraction, and Q&A, a local embedding model all-MiniLM-L6-v2 (384 dimensions) so every message is vectorized without extra API calls. FalkorDB is the only database.
The graph is partitioned into three levels: lk_hq stores Ednovax's own products, features, code, employees, SLA policies, and customer registry; lk_client_<id> is one graph per school, storing groups, people, messages, tickets, promises, decisions, and copies of code references; lk_hub is the central support group where staff forward messages from multiple schools. Tenant isolation isn't enforced by an easily forgotten WHERE school_id = ? — when querying a school's graph, other data is simply invisible. Phone numbers are salted and hashed to become IDs; raw numbers are never stored.
There are 182 named Cypher queries in the system, and the dashboard shows FalkorDB's own execution times in real time. A few key queries: find tickets a new message might relate to by semantics; auto-assign by "who owns the most relevant code"; and blast radius of a change — traverse up three levels along IMPORTS edges to find all code depending on affected files.
Deduplication also uses graph-native thinking: a new ticket first creates SIMILAR_TO relationships with open tickets via the vector index, algo.WCC clusters connected candidates, and the model only judges which are actually the same, merging into the oldest ticket and marking DUPLICATE_OF. Vectors propose, the graph shapes, the model only adjudicates.
Cross-school issue discovery follows the same logic without leaking data: each ticket leaves an abstract Signal at HQ (feature, severity, status, and embedding — school and contact info stripped), similar Signals cluster into Patterns, revealing that multiple schools hit the same bug while each school is only told about its own issue. If raw message text or contact names appear in HQ, the build fails outright.
Code is read, not guessed. When indexing repositories, import relationships become IMPORTS edges, CODEOWNERS becomes OWNS, folders or features.yaml become features, and content defined by each file is indexed with embeddings. Source code itself is never stored — when a ticket opens, the system finds the closest files, reads the source from GitHub, and the model can only mention files and functions it actually saw.
Connected components (WCC) and PageRank: algo.WCC (weakly connected components) groups nodes connected by edges into the same cluster; here it groups candidate tickets linked by SIMILAR_TO relationships to avoid pairwise comparisons. PageRank was originally for web page ranking and in a graph database can score "importance," used to rank tickets, code files, or people. Shortest path explains how two entities are related. These built-in graph algorithms let deduplication and ranking avoid pulling all data into the application layer for computation.
Multi-tenancy and physical isolation: Multi-tenancy means one system serves multiple customers. The common approach is adding a school_id filter to the same table, but one omission leaks data; FalkorDB's multi-graph on a single instance lets each school have its own graph, so cross-school data simply doesn't exist at query time — turning isolation from "discipline" into "structure."
If this system had existed that week
The team kept asking themselves: if this workbench had existed that week, what would have been different?
Each school's "please update our report card format" would become a ticket in its own graph, linked to the report card feature and the message making the request. Because the request content is similar, their Signals would cluster into one Pattern at HQ — instead of a dozen chat windows, you'd see one change requested by multiple schools, and exactly which schools.
A school would no longer be just a name in a chat, but a node with shape. HQ would know how each customer is deployed: which repository, which branch, whether it has a separate codebase. The school that tripped them up had code no one else had; once the deployment was registered and the repository indexed, that batch print function would be just another CodeFile implementing the report card feature, with its imports and owner.
The ticket would point to the code that actually matters for that school. The blast radius query would traverse the IMPORTS edges to find other places depending on report card code. The exception that lived only in one person's head would become a path in the graph. The fix wouldn't count as "done" until the school itself said it was done.
Pitfalls along the way
Not everything went smoothly. The server's CPU was too old to run the sharp image library that transformers.js auto-loads on import, causing the service to crash on the VPS while working fine on a laptop — they only embed text, so they ultimately replaced it with a stub module that's never called. The embedding model downloads on first use, so after deployment the first WhatsApp message had to wait for the download; now every deployment pre-downloads and embeds a test sentence, validating that 384 numbers come back, or the deployment fails.
Vector search alone isn't enough. "It works now" is close to everything, so the model's candidates are the union of four graph queries: the sender's school's open tickets, the semantically nearest, the recently active, and the recently resolved. This hybrid is what makes "reuse before create" actually work. And "thanks, it's fixed now" is the hardest sentence in support — when a teacher has two open issues at once, which one got fixed? Instead of letting the model pretend to know, they record a low-confidence resolution on each possible ticket, for a human to confirm.
Cold start and deployment validation: Embedding models are often large, and downloading them on first call introduces noticeable first-request latency — in a message-driven system, this shows up as "the first message after deployment hangs." The fix is to pre-download the model at build or deploy time and run a smoke test with a known input, validating the output dimension matches expectations, otherwise fail the deployment — a common engineering technique to turn "implicit dependencies" into "explicit contracts."
The graph is the memory; the model is just a reader
The project participated in the WeMakeDevs × FalkorDB Graph Hacks hackathon, in the Company Brain track. The track's requirement: use a graph to connect a company's people, teams, projects, documents, and decisions, understanding how the company operates. The evaluation rule is straightforward — if the product still works without FalkorDB, the graph doesn't count. In Ednovax Support, removing FalkorDB leaves nothing: the graph determines which tickets the model sees, which code it reads, who gets assigned, which tickets are duplicates, which schools share issues, and what Ask is allowed to say.
A few lessons the author summarized are worth noting: let the graph guide the model — the biggest quality jump came from handing the model the right candidate tickets before it reads messages; the model judges, the graph decides what it can judge; privacy is easier as structure than as discipline; test with real scenarios — the test suite runs against a real FalkorDB, using a fictional three-week WhatsApp group full of greetings, prayers, and Pidgin.
Currently the live desk runs one real school, Henson (Benin City), connected to its real GitHub repository, with four teachers played by AI. It started as a hackathon project, but the team says it won't stay one — going forward, this is how Ednovax will support schools. "We paid 250,000 naira to learn this lesson. We hope others won't have to."

Related articles

Chain of Thought Isn't Just for AI: How Humans Can Use CoT to Improve Thinking Quality
Chain of Thought isn't just a prompt trick for large models. Starting from a Hacker News discussion, this article analyzes why CoT works for human thinking too, breaks down actionable ways to transfer it, and highlights the key differences between human CoT and machine CoT.
Cloudflare Teams Up with OpenAI: Closi…
Cloudflare Teams Up with OpenAI: Closing the Loop from Vulnerability Discovery to Defense with AI
Cloudflare uses OpenAI Daybreak to investigate security vulnerabilities and generate reviewable remediation plans, while developers use Codex to boost coding efficiency and quality.

Orca Multi-Agent Orchestration in Practice: A Guide to Parallel Collaboration with Claude and Codex
Orca is Stable AI's free open-source agent workbench supporting 34 agents like Claude Code and Codex in parallel. Learn its Git worktree isolation, multi-model orchestration, and mobile management.