This case study is private.
Return to the homepage and unlock with a password to view this work.
← Back to workReplacing four SaaS tools with one platform that runs the agency end-to-end
Operations platform · Development agency
One internal platform that runs a development agency end-to-end. A signed deal spins up a client and project, developers log time, invoices generate and settle through Stripe — no exports, no spreadsheets, no manual reconciliation. Built solo. Replaced four standing SaaS subscriptions and the master spreadsheet between them.
- 01
The patchwork problem
Four SaaS tools, monthly exports, one fragile spreadsheet holding it together.
The agency ran on disconnected third-party tools: time tracking in one place, invoicing in another, deals tracked manually, with a master spreadsheet glueing it all together at the end of the month.
Every billing cycle was the same routine. Export time. Reconcile it against deal terms. Generate invoices. Send payment links. Chase payments by hand. Knowing how a project was tracking against budget meant doing the math after the fact.
With multiple client projects in parallel, the monthly close had quietly become a part-time job.
- 02
One workflow, top to bottom
Deal → client → project → tracked time → invoice → settlement. One app, one URL.
Built as a single Next.js app with typed entities from the database up. The flow follows how the agency actually operates: a signed deal creates a client and spins up the project that fulfils it. Developers log time against the project's tasks. Budgets aggregate that time live against deal terms. Invoices generate from the same data and route through Stripe, with paid/failed/refunded state syncing back via webhooks.
One source of truth. One URL. No exports.
- 03
Role-based access at the database
Admins manage. Developers log time. Postgres enforces the rules, not the UI.
Two roles, strict scoping. Admins manage users, projects, clients, deals, and invoices. Developers see only the projects they're assigned to and can log time against them. The boundary isn't enforced in the UI — it lives in Postgres as Row Level Security policies, so even a direct API call can't cross the line.
The win isn't the policies themselves. It's that application code stops carrying access checks. Every query is safe by default. A UI bug can't leak data the user wasn't supposed to see, because the database refuses to return it.
- 04
Stripe, the billing surface
Hosted checkout, idempotent webhooks, state that converges no matter what arrives.
Invoices generate in the app and route to Stripe-hosted checkout. Webhooks reconcile paid, failed, partially-refunded, and disputed states back into the deal record.
The hard part wasn't sending invoices. It was making sure that if a webhook arrives twice, out of order, or weeks late after a manual refund in the Stripe dashboard, the local state still converges to the truth. Idempotency keys on every state mutation. Status transitions encoded as a small state machine. The cycle that used to demand manual exports, manual sends, and manual chase-ups now runs as one button and self-corrects as money moves.
- 05
Outcome
The monthly close stopped being a project.
The agency retired four SaaS subscriptions and the standing spreadsheet that lived between them. The manual reconciliation that used to consume the close cycle is gone — there's nothing to reconcile, because there's nothing to export.
Budgets read live instead of springing surprises at month-end. Invoices generate from the same data developers were logging anyway. The team and the client see the same numbers in the same place.
The boring metric: no one talks about billing in standups anymore.