Cadence
The Khaeli CRM, one system of record for the agency, with an assistant that never sends.
- Product design
- Full-stack engineering
- AI engineering

- Product design
- Full-stack engineering
- AI engineering
Overview
Cadence is the CRM Khaeli runs on: who our clients are, what we have proposed, what we have invoiced, what has actually been paid, and the follow-ups we owe people. Inside it lives Kairo, an assistant that briefs you before a call, researches companies, drafts costings, quotations and follow-up emails, and finds new leads on a schedule.
It is an internal product with a product-grade bar, because the brief had a second clause: one day Khaeli should be able to install it for other businesses. I lead it as Head of Engineering and designed the product, building alongside coding agents whose work ships only after review.
The agency in a pocket, installed from the home screen

The Problem
Before this build, Khaeli's commercial life was split across tools that each held a piece of the story. A self-hosted Twenty CRM kept contacts and deals. A separate customer portal app kept projects, invoices and payment chasing, joined to the CRM by nothing stronger than a stored ID. Before those, there was a Frappe CRM fork that never grew past its rebrand. Every system was someone else's shape: deal pipelines and seat pricing language for a business that actually sells engagements, retainers and invoices.
Split data has a compounding cost. You cannot ask "what is going on with this client" and get one answer, you cannot compute cash questions across systems, and you cannot package any of it for a buyer. The deciding constraint, written into the first architecture decision record, was exactly that: a product whose data lives half in a self-hosted CRM and half in a separate app cannot be installed for someone else. Ours can only be worth selling if it is one thing.
Goals and Constraints
- One system of record. Cadence absorbs the CRM and the portal, and both get decommissioned. Until cutover, migration follows one rule: new work starts here, in-flight projects finish where they are, because clients are long-lived and projects end on their own.
- Sellable without SaaS plumbing. Each future buyer gets their own instance and database. The schema carries no tenant column at all, and a CI script fails the build if one ever appears. The investment moves from row isolation to provisioning.
- An assistant that cannot act alone. Kairo proposes, a person applies. It never sends email, never writes a commercial record on its own, and never computes money: figures come from the server with their currency, and the system prompt forbids arithmetic.
- The team's own language. A glossary defines Client, Proposal, Quotation, Touchpoint, Chase, each with the words we deliberately avoid. Stage is a human judgement; what actually happened is derived from the records.
- A small team's stack. One Turborepo: Next.js web app, Hono API on Bun, Postgres with Drizzle, deployed on Railway with the deployment graph kept as code.
The Solution
The data model is a short commercial spine. A Client holds Touchpoints, the communications a person logs with the next step they owe. A Proposal links one Google Doc, because proposals are written in Docs and the CRM should hold the link and the value, not a worse word processor. A Costing prices it internally from the Rate Card; a Quotation is the client-facing price; an Invoice tracks what is owed; a Payment records what arrived, confirmed by a reviewer against imported bank evidence, and is never edited, only reversed with a reason.
Kairo sits beside the spine, not on it. Every skill it works to is server-owned and role-gated, every tool call lands in the audit log, and nothing it drafts becomes a record or an email until a named person applies it through the same validated endpoints a form uses.
One page answers: what is going on with this client?

Process
I wrote the decisions down instead of settling them in chat: the repo carries more than forty architecture decision records, from "invoices are documents, not integrations" to which AI framework runs which workload. The ADRs that got reversed stay published with their reasoning, including the component library we vendored on a Friday and deleted the following Monday once it started fighting our token system.
The workspace design is measured, not eyeballed. I captured a best-in-class CRM's computed styles and bounding boxes from an authenticated session, wrote the measurements up as a reference system, and built an A/B harness that signs into our running app and scores every contracted element against that reference within two pixels and exact color. Composition and density come from the benchmark; the type, the indigo and the domain language stay ours. A motion audit followed the same discipline: 111 findings, each one traced to a numbered plan or an explicit rejection.
The team runs the product while it is built. Staging carries a UAT feedback button that files bugs with the page and device attached, and every release ships an in-app changelog note in plain language.

Key Features
Kairo researches like a careful junior, and shows its work. Ask for a background check and it reads the client record, checks the company register, searches the web, reads touchpoints and payment history, and writes a cited snapshot with a fit verdict computed by code, not vibes. When the evidence is thin, it says so instead of inventing a score. Model vendors hide behind a ghost name, so swapping providers never becomes a user-facing event.

Reports are saved questions. Any staff member assembles a page of Insights: a data source, filters, a metric, a group-by, drawn as numbers, bars, pies or lines. Money is always one stated currency per figure, never silently combined.


Chasing money is drafted, never fired. An overdue invoice drafts its follow-up into a review queue. A person edits, sends from their own mail, and records it; nothing in the queue emails anyone by itself.

The phone build is a product, not a squeeze. Cadence installs as a PWA, opens full screen with an iOS style tab bar and liquid-glass chrome, and puts today's due follow-ups as a badge on the icon.
Records built for thumbs

Log the call before you forget it

What you owe people, in one list

Outcome
Cadence is real and in internal use at Khaeli: staging and production environments deploy from main on Railway, and the team runs structured UAT on staging with the in-app feedback channel. The Twenty instance and the customer portal it replaces are still being wound down under the migration rule, so I will not claim the cutover is done. No external customer runs it yet; that is the ambition the architecture is holding a seat for, not a result.
What I can say is that the properties it was designed around have held: every record a client's money depends on lives in one Postgres schema, and nothing Kairo drafts, costings, quotations, briefs or chases, becomes a sent email or a saved record without a person's name on the action. That gate is code, not policy.
Tools used
Next.js, Hono, Bun, Postgres, Drizzle, Railway



