Recruiting intelligence on a single AI-native engine. Four queries, four systems deleted.
Built on SynapCores CE 1.13.0-ce. No dependencies, no build step —
node server.mjs.
Candidate matching normally needs four systems:
| Need | Usual tool |
|---|---|
| Records and joins | Postgres |
| "Does this resume mean the same thing as this job?" | Pinecone / Weaviate |
| "Who can introduce us?" | Neo4j |
| "Write the outreach" | OpenAI API |
Every arrow between those boxes is a network hop, a sync job, and a chance for drift — the vector index says one thing, the graph says another, and nobody knows which is right.
TalentGraph runs all four inside one engine, in one transaction.
1. Start SynapCores (once):
curl -fsSL https://get.synapcores.com | sh
# Before first boot: the default model is qwen2.5-coder:7b (4.4 GB).
# Swap it for something small in ~/.synapcores/gateway.toml:
# [query.ai_service]
# model = "llama3.2:1b"
printf 'export AIDB_JWT_SECRET="%s"\n' "$(openssl rand -base64 32)" > ~/.synapcores/gateway.env
source ~/.synapcores/gateway.env
/opt/homebrew/bin/synapcores --config ~/.synapcores/gateway.toml --accept-license \
>> ~/.synapcores/gateway.log 2>&1 &
grep -A 14 FIRST-BOOT ~/.synapcores/gateway.log # admin password + API key2. Seed and run:
cp .env.example .env # paste the aidb_… key
node scripts/bootstrap.mjs # schema, 12 candidates, embeddings, graph
node server.mjs # → http://localhost:3000No npm install. Node 18+ built-ins only.
Each one removes a box from that table above.
SELECT name, headline, years_exp FROM candidates
WHERE resume_text ILIKE '%Rust%' OR headline ILIKE '%Rust%'
ORDER BY years_exp DESCThree hits: a weekend hobbyist, someone who did one experiment, a bootcamp grad.
The strongest candidate in the database is not in that list. Dana Kovalenko has eight years of exactly this work — she writes "memory-safe compiled language with zero-cost abstractions and a borrow checker." She never types "Rust", so substring matching structurally cannot reach her.
SELECT c.name, c.headline,
ROUND(COSINE_SIMILARITY(c.resume_vec, j.desc_vec), 4) AS match_score
FROM candidates c CROSS JOIN jobs j
WHERE j.id = 'job_001'
ORDER BY match_score DESC LIMIT 6| candidate | score | |
|---|---|---|
| 1 | Dana Kovalenko | 0.6693 |
| 2 | Tom Achebe (keyword match) | 0.6159 |
| 3 | Priya Raman (keyword match) | 0.6025 |
| 4 | Yuki Tanaka | 0.5149 |
| 5 | Marcus Webb (keyword match) | 0.5076 |
Dana first. Note the keyword matches don't vanish — they rank correctly, beneath the person who actually did the work. Same table, one function call. No second database, no sync job, nothing to drift.
MATCH (j:Job {id:'job_001'})-[:SIMILAR_TO > 0.6]->(c:Candidate)
MATCH (c)-[:WORKED_AT]->(co:Company)
<-[:WORKED_AT]-(peer:Candidate)
-[:WORKED_AT]->(:Company {id:'co_001'})
RETURN c.name AS candidate, co.name AS shared_employer, peer.name AS warm_intro→ Dana Kovalenko · Northwind Data · Sam Osei — in ~20ms.
SIMILAR_TO is a vector hop inside a Cypher pattern. Match the job to candidates by meaning,
then walk their employment graph to find a warm introduction. One statement, one engine, one
transaction.
Dana overlapped with Sam at Northwind Data. Sam is at Vector Labs now. A cold candidate just became a warm introduction.
With Pinecone + Neo4j this is two round-trips, application-layer glue, and two stores that can disagree about what is true.
SELECT c.name,
GENERATE('Write a two-sentence recruiting message to ' || c.name || …,
'{"max_tokens": 90, "temperature": 0.2}') AS draft_message
FROM candidates c WHERE c.id = 'cand_001'Ranking, traversal and generation all happened inside the database. No feature store, no model server, no API key, no data leaving the machine. The model is a 1.3 GB Llama 3.2 running in-process.
Honest observations from a day with the engine — the docs and the shipping binary disagree in a few places, and these cost me real time:
POST /v1/graph/matchtakes the body fieldsql, notcypher. The developers page documentscypher; the binary rejects it withmissing field 'sql'. You still write Cypher./v1/graph/matchis read-only — it acceptsMATCHand nothing else. Cypher writes (MERGE) go through/v1/query/executeinstead.CREATE (n:Label)is rejected there, because the SQL parser expectsCREATE TABLE;MERGEworks and is idempotent.MATCH … SETandUNWIND … MERGEboth fail, but severalMERGEclauses in one statement work fine — so properties get set inline in the pattern and bulk loads become one statement per edge.SIMILAR_TOonly sees vectors stored on the node under the property nameembedding—embandvectorare silently ignored, andEMBED()isn't callable inside Cypher. Sobootstrap.mjsreads the vectors out of the SQL columns and writes them back as literal arrays. Undocumented, and the single most valuable thing I learned.- Health is
/health, not/v1/health. There's noARRAY_LENGTH(); get the embedding dimension from/v1/ai/embeddings→.data.dimensions(384 forall-minilm). ROUND(x::NUMERIC, n)fails —::NUMERICcasts aren't supported. PlainROUND(x, n)works.- Response envelopes are inconsistent:
/v1/query/executeand/v1/ai/embeddingswrap indata;/v1/graph/match,/v1/auth/loginand/v1/users/medon't. - It's much faster than the docs suggest. They say published binaries are CPU-only; the macOS
build links Metal and
GENERATE()returns in ~550ms on an M-series laptop, not the 30–60s the CPU-only figures imply.
None of this is a complaint — a v1.13 engine moving this fast will outrun its docs. But it's exactly the friction a design partner hits in week one, and it's cheap to fix.
- Vertical first: staffing and recruiting agencies. They feel the four-system tax hardest — small teams, no platform engineers, and a direct revenue link between match quality and placement fees. The pitch isn't "AI-native database", it's "delete three vendors and your sync jobs".
- A design-partner POC in two weeks: their real ATS export, their real placement history. Success criterion agreed up front — recall@10 against roles they actually filled, measured against their current keyword search. That's a number a VP of Talent can take to a budget meeting.
- Instrument the wedge: log every query's latency and system-of-origin, so by week one you can show "these 4 services became 1, p99 went from 380ms to 22ms". Land on the pain, expand on the graph — referral networks are where the switching cost gets built.
server.mjs zero-dep HTTP server, named-query catalogue
queries.mjs the four queries — the actual substance
lib/synapcores.mjs minimal client; wraps the two API quirks above
scripts/bootstrap.mjs idempotent seeding + self-verification
sql/bootstrap.sql schema, 12 candidates, 5 companies, 3 jobs, embeddings
sql/graph-edges.cypher employment edges
public/index.html single-page UI, inlined CSS/JS
The dataset is deliberately small and hand-written. Dana's resume is constructed to contain no
rust substring, so query 1 provably misses her — bootstrap.mjs asserts this on every run.
MIT.