All work

Meeting Intelligence Platform

A Gong-class revenue intelligence system built on Zoom, n8n, and pgvector. Every sales call is scored against CRM data, and an AI analyst answers pipeline questions with cited sources.

Role
Architect & sole engineer
Timeline
2025 to present
Status
In production
Context
Built as the internal platform of FullFunnel, a Boston-based GTM agency; extended to a PE-backed enterprise deployment
Next.jsSupabasepgvectorn8nClaudeGemini embeddingsHubSpotSlack

// stage.problem

Call recordings pile up; nobody knows what the pipeline is actually saying

Sales calls carry more signal than any other data a revenue team owns, and most teams never use them. Recordings sat in Zoom. CRM stages said one thing, conversations said another, and forecasting ran on the CRM version. Gong and its class solve this at enterprise prices that a services business and most mid-market teams will not pay.

The goal: capture every call automatically, score it against what the CRM claims about the deal, and make the whole corpus queryable at infrastructure cost.

// stage.system

Zoom to scored, searchable, cited intelligence

System architecture · call to cited answer
Zoomrecordingsn8n orchestratorcentral OAuth8-hour sweephistorical + newSupabasePostgres +pgvector~8 chunks/meeting6 scoring chainsstage-specificvs CRM truthclassify by call type"Ask" analystRAG + citations3 context layersHubSpotsyncDashboardSlackdigestsHard boundary: n8n writes pipeline tables, the app reads them, never the reverse.Failure modes chosen per subsystem: pipeline fails loud in n8n, analytics fail open.Gemini embeddings · Claude answers · 50 queries/day cost guardrail

Capture is centralized: an n8n workflow holds the Zoom OAuth and sweeps recordings, historical and new, on an 8-hour cycle. Nothing depends on reps remembering a bot invite. Each call is classified into one of six stage types (discovery and scoping, follow-up, onboarding, client meetings, internal client meetings, internal) and routed to a stage-specific scoring chain, because the questions that matter in a discovery call are noise in an onboarding call. Scores land next to CRM data: account health, BANT/MEDDIC gaps, and the delta between what the deal stage claims and what the conversation shows.

Transcripts are chunked and embedded into pgvector inside Supabase; the RAG layer gets its own section below. The n8n pipeline and the dashboard never write to each other’s tables. That boundary kept a two-runtime system debuggable.

Digest workflows push weekly summaries and deal alerts into Slack, and scores sync to HubSpot so the intelligence shows up where reps already work.

// stage.product.view

What the team sees every Monday

meeting-intelligence / scorecardAll Analysis ScorecardREP PERFORMANCE AND MEETING SCORE DISTRIBUTIONMEETINGS SCORED580+STAGE-SPECIFIC CHAINS6AT-RISK REVENUE SURFACED$121.9MTEAMnames removed6.96.86.66.45.9MEETINGS BY STAGE TYPECheck-inOnboardingInternal check-inDiscoveryFollow-upInternalASK“What did we tell them about pricing?”Answer streams with sources attachedcites: meeting + moment
INTERFACE RECREATED WITH REPRESENTATIVE DATA · CLIENT AND REP NAMES REMOVED

// stage.deep.dive

Inside the RAG layer

The n8n pipeline splits each transcript into chunks of roughly 500 to 800 tokens, which works out to about 8 chunks per meeting; the corpus held 611 chunks across 76 meetings at the snapshot this section describes. Every chunk carries its meeting ID in metadata. That one field is what makes citations possible later.

Embeddings come from Gemini (gemini-embedding-001, 3,072 dimensions), stored as halfvec in Postgres to halve the storage per vector. The model choice was about consistency: the n8n ingestion pipeline and the dashboard’s query-time embedding must match exactly, or retrieval degrades in ways no error log will report. Retrieval runs cosine similarity over an IVFFlat index, keeps only chunks above a 0.3 similarity floor, and sizes top-k to the question: 8 chunks for a standard query, 12 for long ones, 15 when the question spans reps or the whole pipeline.

The Ask analyst assembles context in three layers with a fixed token budget. Meeting scores get 45%: one line per scored meeting with rep, topic, stage, score, and health. Coaching intelligence gets 35%: strengths, blind spots, deal sentiment, and next actions, pulled only for the meetings retrieval matched. Raw transcript excerpts get the final 20%. Scores frame the answer, coaching explains it, transcripts ground it.

Citations are a SQL join. Because chunk metadata carries the meeting ID, linking an answer back to the exact meeting is a join from the chunks table to the scored-meetings tables inside one Postgres database. No cross-system lookup, no reconciling IDs between a vector store and an application database.

Cost is governed in the product: 50 queries per user per day, plus a burst cap of 10 queries per 5 minutes. LLM cost per answer is real money at team scale, and a limit in the product beats a surprise on the invoice. The rate-limit check itself fails open, so an analytics outage never blocks a legitimate question.

// stage.key.decisions

Why it’s built this way

Decision 01

n8n owns capture, the app owns everything after

Chose
A hard table boundary between pipeline and dashboard
Over
One Next.js codebase doing capture, scoring, and UI

OAuth token refresh, retry sweeps, and rate-limited Zoom API paging are workflow problems, and n8n does them with visual debuggability. The dashboard reads what the pipeline writes and never writes pipeline tables. When something breaks, the boundary tells you which half to open.

Decision 02

Six scoring chains instead of one mega-prompt

Chose
Stage-specific LLM scoring chains
Over
A single universal call-scoring prompt

Discovery quality and onboarding quality are different constructs; one universal prompt flattens them into a single vague grade. Separate chains keep scores comparable within a stage and coachable: a low discovery score maps to specific missing behaviors.

Decision 03

pgvector over a dedicated vector DB

Chose
Embeddings live in Supabase Postgres
Over
Pinecone or a separate vector service

Retrieval always joins transcripts against meetings, deals, and scores. One database means a citation is a single SQL join. It also means one bill, one backup story, one access-control model.

Decision 04

A view for the list, rows for the detail

Chose
A meetings_list view for O(1) list rendering
Over
Selecting from scored_meetings everywhere

Scored meeting rows run ~23K characters with full JSON payloads. The list view projects only what the index page needs; detail pages pay the full cost only when opened. Dashboard went from sluggish to instant without caching infrastructure.

// stage.production.notes

What broke, and what it taught the system

LLMs return prose when you demand JSON

A batch of meetings failed scoring with zero scores because one model returned explanatory text instead of JSON. Fix: hardened prompts, a JSON-repair fallback that extracts the payload from prose, and raised max_tokens so answers stop truncating mid-object. The failed batch re-scored cleanly.

Fail-open for analytics, fail-loud for pipeline

Usage analytics write on a fire-and-forget pattern: an analytics outage should never block a scoring run. The scoring pipeline itself does the opposite. Any failed step is visible in n8n with the exact call that broke. Choosing a failure mode per subsystem beat one global policy.

OUTCOMES

What the numbers say

580+meetings scored across 15 reps
~15Kcalls analyzed in the enterprise deployment
$121.9Mflagged at-risk across $212M of pipeline (conversation vs CRM delta)
6stage-specific scoring chains
$212M PIPELINE ANALYZED$121.9M AT RISKsurfaced by scoring vs CRM stage

Internally, the platform runs across the agency’s sales team: every call scored within hours, weekly Slack digests, and an analyst that answers "what did we tell them about pricing?" with a citation instead of a guess.

The enterprise deployment proved the architecture scales past its origin: ~15,000 calls across $212M of pipeline for a PE-backed client. $121.9M of that pipeline was flagged at-risk, meaning the conversation record contradicted the CRM stage or deal health, along with the specific rep behaviors that correlate with deals advancing. The system the client evaluated against was quoted at ~$200K a year; this one they own.

WHAT I’D DO DIFFERENTLY

The honest retro

  • Chunking strategy deserved more experimentation up front. ~8 chunks per meeting works, but score-aware chunk boundaries (splitting on topic shifts rather than length) would improve citation precision.
  • I built the dashboard before the Slack digests; usage data says digests drive most weekly engagement. Distribution-first would have been the better order.

Neeraj, builder of the machine.

VP of Operations, GTM Agency

// stage.common.questions

What does Gong cost compared to building your own meeting intelligence?

The equivalent enterprise conversation-intelligence contract was quoted at roughly $200K per year; this platform was built instead. The build runs on Supabase, n8n, and per-call LLM costs: infrastructure pricing instead of per-seat licensing. The trade is engineering time; this system took one engineer to architect and ship.

How does the RAG "Ask" analyst avoid hallucinated answers?

Every answer is grounded in retrieved transcript chunks that must clear a 0.3 cosine-similarity floor, and each citation links back to the exact meeting through a SQL join on the chunk’s meeting ID. Context is assembled from real scores and coaching data, so the model quotes the corpus instead of inventing it.

What stack does the meeting intelligence platform run on?

A Next.js dashboard, Supabase Postgres with pgvector for embeddings, n8n for Zoom capture and scoring orchestration, Claude for scoring and answers, and Gemini for embeddings. Scores sync to HubSpot and digests go to Slack.