Backend · API · Live
Identity Reconciliation Engine
The same customer, four email addresses, two phone numbers, six orders. One endpoint that decides they are one person — in two queries, however tangled the history.
Built
2026
Stack
- TypeScript
- Express 5
- Prisma
- PostgreSQL
Hosted on a free tier — the first request after idle takes around 25 seconds to wake.
01 — The problem
Linked records want to become a graph, and a graph wants to be walked.
A customer orders once with a personal email, once with a work email and the same phone, once with a new phone and the work email. Each order links a pair. Do that enough times and the contacts form a graph — and the obvious implementation walks it on every lookup.
That works and it gets slower in a way that is hard to see coming, because the cost depends on how tangled a particular customer’s history happens to be. The customer with the most orders — the one you least want to keep waiting — has the deepest chain.
Hold an invariant instead of walking a graph.
The invariant is one sentence: a secondary contact always points directly at its primary, never at another secondary. Link depth is therefore always exactly one, and a lookup is two queries — find the matching contacts, then fetch that cluster — no matter how many merges the cluster has survived.
The cost does not disappear; it moves. When two clusters merge, every secondary on the losing side is repointed at the surviving primary in the same transaction. That is union-find with eager path compression, with the work paid at write time, where it is rare and bounded, instead of at read time, where it is constant and unbounded.
- Queries per lookup
- 2
- regardless of merge history
- Link depth
- 1
- held as an invariant, not an average
- Endpoint
- POST /identify
- one route, one responsibility
- Compression
- Eager
- paid at merge time, inside the transaction
What it deliberately does not do
-
It does not walk the contact graph. There is no recursive traversal anywhere, by design. If the invariant ever broke, the two-query guarantee would break with it — which is why the merge happens inside a transaction rather than as a follow-up job.
-
It does not run on paid infrastructure. The deployment is a free Render instance that sleeps when idle. The first request after a quiet period takes around 25 seconds — a hosting choice, not a property of the service.