Thirty small tools behind one account, each one a template and a route
A Python platform where adding a tool means adding a template and a router, with no deployment of its own, and subscriptions, orders and webhooks shared across every one of them.
What the engagement involved
Professional Services
20 weeks
Dedicated team
Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.
The hard problem
A company selling thirty small tools will build thirty small applications unless something stops it. Each one then needs its own billing, its own account handling, and its own deployment, and the thirty-first never gets built because the cost is now prohibitive.
Every tool carried its own accounts, its own subscription check and its own deployment. That meant five implementations of billing, which had already begun to disagree: a customer whose card failed was locked out of two tools and not the other three, because each read the subscription state its own way and at its own moment.
One platform with a tool contract: a template and a router. Everything a tool would otherwise reimplement (accounts, subscriptions, orders, search, webhooks) lives once in the engine and is inherited.
The system we were asked to build
The tool vendor sells a drawer of small utilities instead of one large product. We built the platform underneath them: a tool framework, plus the subscription, order and webhook machinery every tool shares.
A vendor selling a drawer of small utilities, not one product, at the point where the trajectory was about to become the problem. Each tool had been built as its own small application, and the fifth had cost as much as the first. On that curve the thirty-first would never be built. It wasn't that it wasn't worth building; adding a tool had simply become a project.
Platform Engineering
How the pieces fit
- 01
Defined a tool as a template plus a router, and made that the only way to add one
The contract was fixed in the first two weeks and then defended: every request to let a tool do something outside it was refused.
- 02
Put accounts, subscriptions and orders in one shared engine that every tool inherits
Extracting the engine meant migrating five tools that each had their own user table, and reconciling the accounts that existed in more than one of them.
- 03
Handled provider webhooks centrally so billing state is consistent across the drawer
Centralizing webhooks resolved a live inconsistency: a customer whose card had failed was locked out of two tools and still had access to three.
- 04
Made search span the tools, because a drawer nobody can search is a drawer nobody opens
Tools register their own name, description and keywords at startup, so search runs over the real registry and never over a hand-maintained list that drifts.
- 05
Kept each tool's own logic isolated so one failing doesn't take the platform with it
Isolation was tested by deliberately breaking a tool in production during a quiet window, because a boundary nobody has crossed is an assumption.
A tool is a template and a router
The contract is narrow on purpose: the thirty-first tool costs what the third did.
A tool is a Jinja template and a FastAPI router. That's the whole contract, and it's narrow on purpose: everything a tool might otherwise want to reimplement is denied to it, which is why the thirty-first tool cost what the third did. A new tool is two files and a registry entry, and it inherits accounts, billing, search and layout just by existing.
- The contract is one template plus one router, nothing else
- A new tool is two files and a registry entry
- The thirty-first tool costs what the third did
The invoice splitter mid-run: a 214-page batch mapped page by page, 41 of 52 invoices written, each file named from the invoice number found on its first page, and invoice 42 being written now.
The Workshop plan at £24.00 a month buying all thirty tools, three caps that reset together on one date, six months of invoices, and the account, orders and search every tool inherits.
One engine, inherited
Users, subscriptions, orders, contact and search live once and every tool gets them.
Users, subscriptions, orders, contact and search are implemented once in the engine and inherited by every tool. No tool holds its own user table or its own notion of who has paid, which means an entitlement question has exactly one answer, and a bug in it is fixed in one place for all thirty tools at once.
- Accounts, subscriptions, orders and search live once
- No tool holds its own user table or entitlement logic
- One fix lands across all thirty tools
Central webhooks
A billing change lands in one place, so every tool resolves the same entitlement.
Stripe webhooks land in one handler that resolves the subscription state and writes it to the shared entitlement record, so every tool reads the same answer at the same moment. Events are verified, deduplicated and idempotent (a retried webhook re-reads and exits instead of double-applying), and an unrecognized event type is logged, never swallowed.
- One verified webhook handler resolving all entitlement
- Deduplicated and idempotent: retries can't double-apply
- Unrecognized event types logged, never swallowed
The central webhook router in the vendor's console. Every event is verified and deduplicated, duplicates re-read and exit, an unrecognised type is logged, and a refund that timed out after five attempts stays visible beside the shared entitlement record.
The search index built from the tool registry. The same query ranks differently for an account on a smaller plan and one on Workshop, and the matched tool's registered name, description and keywords sit beside the results.
Search across the drawer
Thirty tools nobody can search is a drawer nobody opens twice.
Thirty tools with no search is a drawer nobody opens twice. Every tool registers its name, description and keywords into one index at startup, so search runs over the actual registry, never a hand-maintained list that drifts. Results rank by relevance and by whether the user's plan includes the tool, so the answer is something they can use.
- Tools register their own search metadata at startup
- The index is the registry, so it can't drift
- Results ranked by relevance and by plan entitlement
Isolated failure
Every run from every tool in one list. The label maker's failure is a single row with its own error, logged with the tool named, while the runs started after it finished.
A broken tool degrades to itself and leaves the platform running.
Each router is mounted with its own error boundary, so a tool that throws returns its own error page and the platform, the nav and the other twenty-nine keep working. Tool-level failures are logged with the tool name attached, which turns a platform-wide alarm into a specific ticket about a specific tool.
- Per-tool error boundary at the router mount
- A broken tool degrades to itself, not to the platform
- Failures logged with the tool named, so alerts are specific
Phase by phase
Phase 1: The Tool Contract
A Template And A Router
Defined what a tool is, narrowly, so adding the thirty-first costs what the third did and the cost never compounds.
- Tool Contract
- Template Convention
- Router Registration
Phase 2: The Shared Engine
Everything A Tool Should Not Reimplement
Moved users, subscriptions, orders, contact and search into the engine, so a tool author writes the tool and inherits the rest.
- User & Auth
- Subscription Model
- Order Handling
- Search
Phase 3: Billing That Holds
Webhooks In One Place
Centralized provider webhooks so a subscription change lands once and every tool sees the same entitlement. No tool holds its own opinion about who has paid.
- Webhook Router
- Entitlement Resolution
- Billing Reconciliation
Phase 4: Isolation
One Tool Failing Is One Tool Failing
Kept per-tool logic isolated behind the contract so a broken tool degrades to itself instead of taking the drawer down.
- Failure Isolation
- Per-Tool Errors
- Health Surface
Seats and access, held in the account: two people on the plan, three invitations waiting on an upgrade, and one set of permissions that applies to all thirty tools.
What it carries now
−88%
Cost to add a tool
1
Billing implementations
0
Deployments per new tool
30+
Tools sharing one account
Cost to add a tool compares engineering time for tools built after the platform with the average of those built before. Billing implementations is a count. Deployments per new tool is zero because a tool is a registry entry. Thirty-plus tools sharing one account is the current state of the drawer.
Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.
What the architecture settled
Thirty small products become thirty small maintenance burdens unless the contract between them is narrow.
The fifth tool had already cost as much as the first, which is the curve that ends with the thirty-first never being built at all.
Billing implemented per-tool is billing that will disagree with itself by the fifth tool.
Per-tool billing had already begun to disagree at five, and the disagreement was invisible until a customer was locked out of some tools and not others.
The real test of a tool platform is what the thirty-first one costs, not the third.
The third tool tells you almost nothing. The thirty-first is the number that says whether the platform worked, and it's the one worth designing against.
Isolation is what lets you ship a speculative tool: the downside is bounded to itself.
Bounded downside is what makes a speculative tool worth shipping: with isolation, a bad one costs its own build and nothing else.
How the work was run
We wrote the tool contract first and migrated three existing tools onto it before adding anything new, which is what proved the contract was narrow enough. Billing was centralized in the same pass, because two tools disagreeing about a subscription is the failure that erodes trust fastest.
A dedicated team for twenty weeks, with the contract (a template and a router) fixed in the first two weeks and then deliberately never widened. Every request to let a tool do something the contract didn't allow was refused, which is why the contract is still narrow. The thirty-first tool costing what the third did is entirely a consequence of that refusal.
One failed card, replayed
Five tools gave one customer five answers. One record gives thirty tools one.
The same card payment failing, first against the five billing implementations the drawer had at five tools, then against the single entitlement record the webhook handler writes now, and finally with the webhook delivered a second time. Switch tabs, or use the arrow keys once one is focused.
Each of the five tools carried its own accounts and its own subscription check. The same failed card reached each of them differently, and the customer ended up locked out of two tools and still inside the other three.
- Card payment fails
- PDF merger reads it its own way
- CSV cleaner reads it its own way
- Batch resizer reads it its own way
- Invoice splitter reads it its own way
- Label maker reads it its own way
Where billing state lived
Five places, one per tool
What each tool decided
- PDF mergerAsked the provider on each runLocked out
- CSV cleanerIts own webhook endpointLocked out
- Batch resizerRead the plan at sign-inStill open
- Invoice splitterIts own subscriptions tableStill open
- Label makerPlan cached on the sessionStill open
2 locked out, 3 still open: one customer, one failed card, five answers.
Why they can no longer disagree: a tool is now a Jinja template and a FastAPI router and nothing else. It has no billing code of its own, only the engine’s answer.
From a billing event to thirty tools giving the same answer
Everything a tool would otherwise reimplement sits in the engine, and a tool only adds a template and a router. A new tool is two files and a registry entry, with no deployment of its own.
- 01 · SourceStripe eventA subscription change arrives once, at one endpoint, and every tool reads the result.
- 02 · HandlerOne verified webhook handlerVerified, deduplicated and idempotent: a retried event re-reads and exits, and an unrecognised type is logged, never swallowed.
- 03 · StateEntitlement record in PostgreSQLUsers, subscriptions and orders live once in the engine, so an entitlement question has exactly one answer.
- 04 · RegistryTool registryA tool is a Jinja template plus a FastAPI router. Each registers its name, description and keywords at startup, so the search index is the registry.
- 05 · DeliveryRoutes behind error boundariesEach router is mounted with its own error boundary. A tool that throws returns its own error page, logged with the tool named.
What one failure can reach
Billing consistency & failure isolation
One entitlement record, so tools can't disagree
No tool holds its own user table or its own notion of who has paid. Every tool reads the same record at the same moment, and a fix to it lands in all thirty at once.
Retries cannot double-apply
Webhooks are verified, deduplicated and idempotent, so a retried event re-reads and exits. An event type the handler doesn't recognize is logged, never swallowed.
A broken tool degrades to itself
Each router has its own error boundary, and failures are logged with the tool named. Isolation was tested by deliberately breaking a tool in production during a quiet window.
Selling a set of tools that each bill their own way? Scope your build in 3 minutes.
Scope your buildNearby engagements
AI & AutomationA private legal assistant grounded in verified precedents
A private knowledge assistant that searches internal case files and precedents, providing cited answers legal teams can verify in seconds.
Legal & Law Firms · 14 weeks
Product DesignAn onboarding flow that guides trial users to value
A redesigned SaaS trial onboarding experience with progressive checklists, sample data, and inline guidance that turns signups into active subscribers.
Professional Services · 10 weeks
Product DesignA design system that brought speed and consistency to 4 product teams
A token-based design system in Figma and React that eliminated component duplication across 4 product squads and cut the time from design handoff to merged frontend.
Professional Services · 14 weeks
Let's talk
Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.














