I've built commission systems for insurance agencies more than once. Commission automation at Plural. A custom CRM with policy and commissions management at FHK Insurance, used by agents nationwide. Years inside a 400K-member health insurance marketplace at Catch Health. Every one of those engagements eventually hit the same wall: the carrier's commission statement and the agency's book of business don't agree, and nobody can say exactly where or why.
Carrier underpayments aren't malicious. They're systemic. Enrollment feeds break. Plan codes change mid-year. Effective dates get recorded wrong. Terminations process twice. The carrier's system and your book drift apart a little every month, and most agencies reconcile by spot-checking the big accounts and trusting the rest. The money that falls through that gap is real, and there's a clock on disputing it.
So I built CommissionSight. It ingests carrier statements, matches every payment to a policy, member, and producer, and flags underpayments, missing statements, and split errors. Billing went live today.
The product story is simple. The platform story is where I learned things, so that's what this post is about.
The Shape of It
CommissionSight runs on Cloudflare. A Hono API on Workers handles HTTP, the ingest queue consumer, and a cron that runs every fifteen minutes. Raw statement files land in R2. Ingest runs through Cloudflare Queues with retries and a dead-letter queue. The marketing site, docs, admin, and the app itself are static-asset Workers.
The data model is split on purpose:
- A control plane in D1. Accounts, API tokens, carrier configs, jobs, webhooks, billing, and dashboard rollups. Small, hot, and global.
- A dedicated Postgres database per account. Every agency's statements and member data live in their own Neon database. The control plane holds an encrypted pointer to it, and that's all.
That second decision is the one I'd defend the hardest.
Lesson 1: Isolation Should Be Physical
Commission statements are full of names, policy numbers, and dates of birth. The standard SaaS move is one big database with a tenant_id on every row and a lot of faith that every query remembers its WHERE clause. It works right up until one query doesn't.
With a database per account, the worst bug in my query layer can only ever read the wrong rows inside the right agency. Another agency's data isn't behind a filter. It's in a different database the request never has credentials for. Isolation stops being a code-review discipline and becomes a property of the architecture.
That's more databases to operate. Serverless Postgres makes that cheap enough that it's the right trade for regulated data, and I'd make it again on day one.
Lesson 2: Ask About Scale at the Right Level
Every prospect asks some version of "does it scale?" The honest answer turned out to depend entirely on what you're scaling.
A Worker gets 128 MB of memory. Before launch I measured what parsing a statement actually costs: roughly 114 MB at 40,000 rows, and roughly 883 MB at 400,000. The math is not subtle. A single enormous statement file will never fit in one Worker invocation, no matter how clever the parser is.
And that's fine, because it's the wrong unit. Agencies don't need one four-hundred-thousand-row file processed in one shot. They need thousands of monthly statements, from dozens of carriers, across an entire book, processed reliably. So the per-file limit is explicit: uploads over 15 MB get a clear 413, and a statement tops out at 40,000 rows. The book, the thing that actually grows, has no such ceiling. Each statement is its own queue job, upserting in batches of 500 with deterministic IDs, so work spreads out horizontally instead of piling up in one process.
So when someone asks whether it scales, I don't say "yes." I say: per statement, here's the bound and why; per book, it scales with the queue. That answer is less exciting and much more useful. It's also the answer I'd want from any vendor I was evaluating.
Lesson 3: A New Carrier Should Be Data, Not Code
Every carrier formats its statements differently, and every carrier changes its format without telling anyone. If each new carrier means a code change and a deploy, you've built a professional-services business with a SaaS logo on it.
In CommissionSight, a carrier is a JSON configuration: where the columns are, what they mean, how dates and amounts are written. The ingest pipeline reads the config, maps the rows, assigns stable SHA-256 IDs so re-uploading the same statement is harmless, and runs a delta engine that marks every member green, yellow, or red against what the agency expected to be paid. Adding a carrier is a new config file. Zero code.
Lesson 4: Deploys Should Be Correct by Default
This is the one that bit me, and it's the one I'd most want another builder to hear.
Early on, production settings like the environment name and the public API URL were passed as flags on the deploy command. It worked every time I remembered the flags. The first time a deploy went out without them, production quietly flipped to development settings, pointed at a localhost API URL. Nothing crashed. It was just wrong.
The fix wasn't "be more careful." It was making the wrong deploy impossible. Production values now live in the Wrangler config itself, so a bare deploy is a correct deploy. And the deploy script doesn't consider itself done until the live /health endpoint reports the exact git SHA that was just shipped. If it doesn't match, the deploy failed, whatever the CLI said.
Any process that depends on a human remembering something will eventually fail on the day that human is tired. That's true of deploys, and it's just as true of commission reconciliation, which is the whole reason this product exists.
Lesson 5: Your Admin Gate Is Part of Your Tenancy Model
One more, found and fixed before launch. The admin portal originally trusted a role cached on the session. A cached role is a claim that was true at some point in the past. Access to cross-account admin tools is now checked against an explicit allowlist on every request. When the blast radius is every customer, "was true at login" isn't good enough.
From First Commit to Live Billing
The first commit landed yesterday afternoon. Live Stripe billing was on this afternoon. That speed isn't a boast about typing. It's what happens when the platform gives you queues, object storage, cron, a global edge, and cheap per-tenant databases without a single server to patch. Cloudflare let me spend the time on the questions that actually matter: where the limits are, where the tenants are separated, and what happens when someone forgets a flag.
If you run an agency and you've never reconciled your book member by member, the first statement is usually the most interesting one.
CommissionSight is live at commissionsight.com. If you're building on Cloudflare and want a second set of eyes on your tenancy model or your scale limits, book a call.