Catalyst Revenue Partners · 2026 to now
Catalyst Portal
Commission automation for a channel-sales brokerage. Deals live in the customer's own CRM, the portal reads them in, watches each one hit its milestones, and turns those milestones into money owed across reps, managers and the house.
C# · .NET 10 · EF Core 10 · PostgreSQL · React 19 · TypeScript · Vite · TanStack Query · MUI · AG Grid · Stripe · QuestPDF · Docker · Cloud Run · Firebase Auth
How it is put together
A .NET 10 API in five layered projects, on EF Core against PostgreSQL, with services registered by convention scanning rather than by hand. Thirty-seven controllers and forty-three entities behind it. Authentication is Firebase, and authorisation is a seeded permission catalogue enforced by an attribute on the endpoint and mirrored by a guard on the route, so a deep link can never reach a surface the sidebar is hiding.
The React client is Vite and TypeScript with MUI and AG Grid, and its entire API layer is generated from the OpenAPI document into TanStack Query hooks. Nothing is allowed to call the API by hand, which means a backend contract change surfaces as a TypeScript error rather than a bug in production.
Reading somebody else's CRM
Each customer company connects either HubSpot or Salesforce over OAuth, one CRM per company. Their pipeline stages get mapped once to a shared set of milestones, and from then on tagged deals get pulled in on a watermark, idempotently, so a re-run does not double anything up.
Both integrations are registered applications rather than pasted API keys, checked in alongside the backend, so connecting a CRM is a click and an Allow screen. Behind them sits token refresh, per-provider clients and mappers that turn either shape into one internal deal.
Turning milestones into money
A rules engine resolves who is owed what when a deal moves: finder fees, commission on annual value, a slice for the manager above, and the house take. Rules are effective-dated and versioned, so changing a rate next quarter does not silently rewrite what somebody earned last quarter.
Everything then moves through approval into settlement: batch approval with notes, guards so nobody approves their own money, disputes, reversals and reissues, period locking, payout runs, and server-rendered PDF statements. Every write is stamped into a change log by an interceptor at the database layer, so the audit trail is not something a developer has to remember to write.
Money and time have invariants enforced across the whole project: every timestamp is UTC with a timezone, every date range is half-open, every monetary row carries its currency, and a historical rate is resolved from effective-dated data rather than replayed out of the audit log.
Testing a system that moves money
788 tests, run against a throwaway PostgreSQL container rather than an in-memory database. That decision has a comment beside it in the project file, because in-memory providers lie about unique indexes, transactions and decimal precision, and a commission system cannot be tested against something that lies about decimals.
Each test gets a fresh dependency-injection scope on a shared container with the real audit interceptor wired exactly as production, alongside fakes for the CRM clients, Stripe and email, and a builder for composing commission scenarios. Playwright covers the commission flow, payout reports and table state on top of that, and the whole suite has to pass before either environment deploys.
Running it
Frontend on Firebase Hosting, API in a container on Cloud Run, database on Cloud SQL, secrets in Secret Manager, all in one Google Cloud project with separate dev and production triggers. Hosting rewrites the API path through to Cloud Run, so the frontend and the API share an origin in every environment and there is no CORS in production.
Cloud Run scales to zero, which means there is nothing sitting there to run a nightly job. Scheduled work is instead a locked-down endpoint called on a schedule, idempotent per job per day, so calling it twice does nothing the second time. Migrations run on boot, so a bad database fails the health check and traffic stays on the previous revision.