Command Hub
An AI command center for an ad agency: meeting intelligence, client memory, and team operations in one internal tool.
- Role
- Developer, sole author of the application codebase
- Stack
- TypeScript · React · Vite · tRPC · Postgres · Drizzle · Gemini · BigQuery · Cloud Run · Docker
- Year
- 2026
Internal production tool for an ad agency. No public link. Screenshots are shared with the agency's permission, with some details redacted.
Context
An account manager at the agency needed four tools open to answer one question about a client: the project management system for the work, the CRM for the relationship, the analytics warehouse for the numbers, and a folder of meeting recordings for what was actually said.
So questions like "what did we agree with this client in March" got answered from memory, or not at all. Nobody was going to re-watch a two-hour recording to check.
Command Hub is the agency's internal operations platform: one place where a client's whole history is unified and can be asked questions in plain English.
What I built
- Meeting transcripts sync in automatically from the tools the team already records with, and are deduplicated on the way in.
- Each meeting is matched to the right client account automatically, with a review queue for the ones the matcher is unsure about. The record organises itself instead of depending on someone remembering to tag it.
- An assistant you ask in plain English ("what did we agree about reporting?") that answers from that client's actual meeting history and cites the meetings it used.
- AI digests, so a long meeting becomes something readable afterwards.
- Project management, CRM and analytics data pulled into one client view, instead of four browser tabs.
- Voice notes with transcription, for the things that never make it into a system.
- Any transcript, email or research document can be copied in full or exported to PDF. The data can leave the tool.
- Role-based access with password login and TOTP two-factor, plus an audit log.
Architecture
- Shape
- TypeScript throughout: a client, a server and a shared package, so types cross the boundary once instead of being redeclared on each side.
- Front end
- React on Vite, with Radix primitives, TanStack Query for server state and Tailwind for styling.
- API
- tRPC over Express (eleven routers), so the client calls the server through the same types the server is written in, and a renamed field is a compile error rather than a runtime surprise.
- Data
- Postgres via Drizzle ORM: 30 tables across 20 migrations. Postgres full-text search on the text-heavy tables, with the GIN indexes declared in the schema so the migration tool cannot drop them as drift.
- Sync
- Eight independent Cloud Functions, one per integration: transcript sync, deduplication, email, CRM, background research, mapping sheets, health checks. Each fails on its own without taking the app down.
- AI
- Gemini through its OpenAI-compatible endpoint, for chat, digests, reports and transcription.
- Infrastructure
- Google Cloud Run, multi-stage Docker on node:22-slim, with Secret Manager for credentials and Cloud Storage for files. GitHub Actions deploys on push.
The hardest part: making the memory work
Asking an assistant about a client's history sounds like a solved problem: put the transcripts in a database, search them, hand the results to a model. It is not, and the reason is worth explaining, because it is the part of this project I would actually defend in an interview.
Keyword search was built first, and measured. On a question spanning four months it returned about thirteen documents, skewed towards recent ones. Thirteen out of hundreds. Worse, it could not bridge vocabulary at all. Someone asking "did they raise a concern about reporting?" needed a meeting summarised as "leadership requested specific cost based metrics currently missing from data tools". Those two sentences mean the same thing and share no meaningful word.
The instructive part was what happened next. Feeding that keyword shortlist to the model made the answer worse: a flagship test question went from two correct answers in three attempts to zero in five. A model trusts a confident list. Giving it the wrong shortlist is worse than giving it none, because it stops looking.
That measured failure is what the rest of the design is built on. It arrived as five layers: digests of every item, period rollups, a timeline map, two-pass retrieval, and semantic search.
- Digests: every meeting gets a short summary, and the record tracks whether it was extracted from the client's own notes or generated, so the two are never confused.
- Rollups: per-period summaries, so a four-month question does not start by reading four months of transcripts.
- Two-pass retrieval: the model first reads the map and the period summaries and names the specific documents it wants; only then are those fetched verbatim. Orient, then drill.
- Semantic search: meeting summaries are embedded so a question matches a meeting by meaning rather than by shared words. This is the layer that bridges the vocabulary gap.
Two decisions in there are the ones I would point at. First: the two-pass step is strictly additive. If it returns nothing, errors, or is switched off by an environment variable, the answer is built from exactly what it would have been before. The feature can improve an answer but it cannot regress one. Second: the semantic layer has a similarity floor. Below it, it returns nothing and lets the model read the map itself, because the whole lesson of the keyword experiment was that a confident wrong shortlist does more damage than an empty one.
The embeddings are stored as a plain array of floats and compared in application code, deliberately not in a vector database. A single client's whole history is tens of meetings, so scoring all of them is a millisecond of arithmetic over a few hundred kilobytes. A vector extension earns its keep at millions of rows; here it would have been an extension, an index type and a migration path bought for nothing. The threshold for revisiting that is written down in the schema.
How AI was used to build it
The product uses AI. More usefully, so did building it, and I can show the mechanics rather than assert them.
The work is driven from Claude Code against a separate context repository that holds a CLAUDE.md project intelligence file, month-by-month implementation logs, and architecture decision records. That file opens by describing itself as the single source of truth for any AI assistant working on the project, and it is written to survive the code repo being deleted and re-cloned. A fresh session reads it and is immediately current, which is the whole point.
The implementation logs record every failure and why, not just what shipped. That is the file I reach for most, and it is the reason a five-month project does not lose its own history.
It shows up in the codebase too: the code comments carry the measurement behind a decision, not just its description. The note explaining why embeddings are not in a vector database includes the arithmetic and the row count at which to reconsider. That is deliberate: it is what makes the next session, human or agent, able to change the decision responsibly.
The project began as a prototype on a vendor AI platform. I forked it into an independent codebase in April 2026, removed every dependency on that platform, and have evolved it since. 231 commits between April and September 2026, all authored by me on this codebase.
On the division of labour, because it is easy to overstate: agents write a large share of the implementation. I decide the architecture, choose what gets built and in what order, measure whether it actually worked, review everything that lands, and own what ships. The keyword-versus-semantic experiment above is the shape of the job: the agents were fast, but the decision to test the assumption instead of trusting it was the part that mattered.
Outcome and current status
In production use at the agency. Version 1.0.0 shipped in late August 2026, the release where the memory architecture landed and the product moved from beta to live. Two patch releases have followed: transcript and document export, then a round of items raised in the team's own review sessions.
Still shipping weekly. The roadmap runs from summarisation, which is where it is now, through analysis and into automation.
No public link: it is an internal tool, and the screenshots here are redacted.
Screenshots
Task details, meeting content and account figures are redacted. Layout and behaviour are unchanged.