Skip to content

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.

The drawer

30 small tools, sorted by what they operate on · nothing to install

Sort: what they operate onSearch 30 tools/NA

Tools in the drawer

30

Installed

0 every tool opens in the account

Runs this period

1,840of 5,000

Plan

Workshop£24.00 a month

PDFs

5 tools
PDF mergerJoin files in the order you drop themToday
Page splitterCut a PDF by range or every n pages
PDF shrinkerSmaller files for email attachments
RedactorBlack out text, then flatten the pageToday
Signature stampPlace a saved signature on each page

Invoices & receipts

5 tools
Invoice splitterOne scanned batch into one file per invoiceRunning
Invoice readerSupplier, date, VAT and total to a rowToday
Receipt sorterPhotos of receipts filed by monthToday
VAT number checkValidate a list of UK VAT numbersToday
Remittance builderA remittance note from paid invoices

Spreadsheets & CSV

5 tools
CSV cleanerTrim, dedupe and fix encodingsToday
Sheet joinerMatch two sheets on a shared columnTue
Statement converterBank statement PDFs to CSVToday
Date fixerMixed date formats to oneToday
Quick totalsGroup and total without a pivot table

Images

5 tools
Batch resizerA folder of images to one sizeToday
Format converterHEIC, PNG, WebP and JPEG
Background removerProduct shots on white or clear
Photo to textText out of a photo or a scan
WatermarkerA mark on every image in a set

Text & letters

5 tools
Letter mergeOne letter per row of a sheetToday
Label makerAddress labels on standard sheetsToday
Document compareWhat changed between two versionsMon
Word counterCounts, reading time and headings
Case fixerNames and addresses in proper case

Links & web

5 tools
QR codesCodes for a link, a card or wifiToday
Link checkerEvery broken link on a page
Page to PDFSave a web page as a clean PDFToday
Campaign linksTagged links from a sheet of URLs
Icon setEvery icon size from one image

What the engagement involved

Industry

Professional Services

Duration

20 weeks

Cooperation model

Dedicated team

Services
Platform engineeringSubscription billingTool framework
Integrations
PaddleProvider webhooksTransactional emailObject storage
Technologies
PythonFastAPIPostgreSQLJinjaStripeWebhooks
Team
1 Project lead2 Backend engineers1 Full-stack engineer1 QA engineer

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.

Introduction

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

01

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.

What shipped
  • 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
Invoice splitter

Invoices & receipts · one scanned batch into one file per invoice · included in Workshop

RunningSearch 30 tools/NA

supplier-invoices-sept.pdf

214 pages · uploaded 14:31 by Nell Achebe

41 of 52 invoices writtenpage 163 of 214 read

Split on a new invoice number · each file named from the number it found

PauseDownload 41 files

The batch, page by page

214 pages
In a written fileBeing writtenNot read yet New invoice

Now writing · invoice 42 of 52

pages 164–168
INV-021082
p.165
p.166
p.167
p.168

→INV-021082.pdf

Written files

41 of 52 · named from the invoice number
#FileSupplierPagesStatus
42INV-021082.pdfCastleway Stationers164–168Writing
41INV-021073.pdfOakfield Print Co.161–163Written
40INV-021064.pdfLowther Utilities157–160Written
39INV-021061.pdfMarram Cleaning Ltd153–156Written
38INV-021052.pdfKestrel Office Supply148–152Written
37INV-021043.pdfPennant Telecom145–147Written
36INV-021040.pdfBrindley Couriers141–144Written
35INV-021031.pdfCastleway Stationers135–140Written
34INV-021022.pdfOakfield Print Co.133–134Written
33INV-021019.pdfLowther Utilities129–132Written
32INV-021010.pdfMarram Cleaning Ltd124–128Written
31INV-021001.pdfKestrel Office Supply121–123Written
30INV-020998.pdfPennant Telecom117–120Written
29INV-020989.pdfBrindley Couriers113–116Written
28INV-020980.pdfCastleway Stationers108–112Written
27INV-020977.pdfOakfield Print Co.105–107Written
26INV-020968.pdfLowther Utilities101–104Written
10 more to write · pages 169–214 not read yetEvery run also lands in Runs
On screen

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.

Plan & billing

One plan buys the whole drawer · billed monthly in pounds

Visa ending 4417Search 30 tools/NA

Current plan

Active

Workshop

£24.00 a month · renews 1 Oct 2026

All 30 tools 2 seats Every run in one list Caps shared across tools
Change planCancel plan

Caps this period

All reset together on 1 Oct 2026
Runs1,840 of 5,000
Batch pages6,412 of 20,000
File storage3.1 GB of 10 GB

One period for the account. No tool keeps its own counter, so a run anywhere counts once, here.

Invoices

Last six months
InvoiceDateForAmountStatus
PB-2026-09-44711 Sep 2026Workshop · monthly£24.00Paid
PB-2026-08-39281 Aug 2026Workshop · monthly£24.00Paid
PB-2026-07-33901 Jul 2026Workshop · monthly£24.00Paid
PB-2026-06-28471 Jun 2026Workshop · monthly£24.00Paid
PB-2026-05-23111 May 2026Workshop · monthly£24.00Paid
PB-2026-04-17961 Apr 2026Workshop · monthly£24.00Paid
Billing contact: Nell Achebe Download all as PDF

What the plan covers

30 tools · one entitlement
PDFs
Invoices & receipts
Spreadsheets & CSV
Images
Text & letters
Links & web

Inherited by every tool

Sign-inSubscriptionOrders & runsSearchContact
On screen

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.

02

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.

What shipped
  • Accounts, subscriptions, orders and search live once
  • No tool holds its own user table or entitlement logic
  • One fix lands across all thirty tools
03

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.

What shipped
  • One verified webhook handler resolving all entitlement
  • Deduplicated and idempotent: retries can't double-apply
  • Unrecognized event types logged, never swallowed
Webhooks

Stripe events · one handler resolves the subscription and writes the shared entitlement record

1 needs attentionThu 17 Sep 2026
Signature verifiedEvery event, before anything else
DeduplicatedBy event id, so it lands once
IdempotentA retry re-reads and exits
Unknown types loggedNever swallowed

Events

Today · newest first
ReceivedEvent typeAccountOutcome
14:32:07invoice.paidHarrow & Finch BookkeepingApplied
14:29:51charge.refundedQuayside LettingsTimed out · 5 attempts
14:26:14invoice.payment_failedLomond JoineryDuplicate · exited
14:26:12invoice.payment_failedLomond JoineryApplied
14:18:40customer.subscription.updatedPetal & Pine FloristsApplied
14:11:03customer.tax_id.createdArden PhysioUnrecognised · logged
14:07:22checkout.session.completedNinefold StudioApplied
13:58:45customer.subscription.createdNinefold StudioApplied
13:52:13invoice.paidWexcombe SurveyorsDuplicate · exited
13:52:10invoice.paidWexcombe SurveyorsApplied
13:40:31customer.subscription.deletedTarn Street CaféApplied
13:31:02invoice.payment_failedBrightwater TutorsApplied
13:22:48invoice.paidKiln Row CeramicsApplied
13:15:19customer.subscription.updatedHarrow & Finch BookkeepingApplied
13:04:55invoice.paidGreyfriars DentalApplied

charge.refunded

evt_3QmT8r2LkXw41c
Quayside LettingsRefund of £24.00 · Workshop planTimed out

Handler attempts

114:29:51Handler timed out
214:30:21Handler timed out
314:31:21Handler timed out
414:33:21Handler timed out
514:37:21Handler timed out

Stopped after five attempts. Not applied, and kept here in the list instead of disappearing into a log nobody reads.

Shared entitlement record

read by all 30 tools
accountacct_quayside_lettingsplanworkshopstatusactiveperiod_end2026-10-01last_eventevt_3QmR1a9PbZq07e
Replay eventCopy payload
On screen

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.

Search index

Built from the tool registry at startup · ranked by relevance and by the account's plan

30 of 30 tools registeredThu 17 Sep 2026
invoice8 matchesAs Arden Physio · PocketAs Harrow & Finch Bookkeeping · Workshop

Results for Arden Physio

Pocket plan · 8 of 30 tools included
#ToolRelevanceOn their planWorkshop
1Invoice readerSupplier, date, VAT and total to a rowIncluded#2
2Invoice splitterOne scanned batch into one file per invoice Workshop#1
3Page splitterCut a PDF by range or every n pagesIncluded#5
4Remittance builderA remittance note from paid invoices Workshop#3
5Receipt sorterPhotos of receipts filed by month Workshop#4
6PDF mergerJoin files in the order you drop themIncluded#7
7VAT number checkValidate a list of UK VAT numbers Workshop#6
8Statement converterBank statement PDFs to CSV Workshop#8
Same index, same query: what the account can use rises, so the first answer is one they can open.

Registered by the tool

At startup, from the registry entry

slug

invoice_splitter

name

Invoice splitter

description

One scanned batch into one file per invoice

files

template + router

keywords

invoicesplitbatchscan

The index is the registry

Rebuilt every startup
Entries30, one per registered tool
Hand-maintained listNone, so nothing drifts
RankingRelevance, then plan entitlement
A tool removedGone from results next startup
On screen

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.

04

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.

What shipped
  • 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
Runs

Every run from every tool, in one list · 1,840 this period

Export CSVSearch 30 tools/NA
All 30 tools▾Both people▾Today · 17 Sep 2026▾Succeeded 12Running 1Failed 1

Today

Newest first · whichever tool started it
StartedToolInputByOutputStatus
14:31Invoice splittersupplier-invoices-sept.pdfNell Achebe41 of 52 filesRunning
14:26CSV cleanersupplier-ledger-export.csvTom Reyes1 fileSucceeded
14:22Label makerclient-addresses-q3.csvTom Reyes—Failed

Label maker returned its own error page. Nothing else stopped.

The drawer, the nav and the other 29 tools kept working: the two runs above started after it. Logged with the tool named: tool=label_maker run=r_7Hq2c

Run again
14:09Batch resizeroffice-move-photos/Nell Achebe48 imagesSucceeded
13:55Letter mergeyear-end-reminder.docxNell Achebe112 lettersSucceeded
13:40Statement converterstatement-aug-2026.pdfTom Reyes1 fileSucceeded
13:12PDF mergerengagement-letters (6)Nell Achebe1 fileSucceeded
12:58VAT number checknew-suppliers.csvTom Reyes23 checkedSucceeded
12:30QR codesharrowfinch-bookingNell Achebe1 imageSucceeded
11:47Invoice readerinbox-invoices (14)Tom Reyes14 rowsSucceeded
11:20Date fixerpayroll-dates.csvTom Reyes1 fileSucceeded
10:54Redactorclient-pack-lowther.pdfNell Achebe1 fileSucceeded
10:12Receipt sorterreceipts-sept (31)Tom Reyes31 filedSucceeded
09:41Page to PDFhmrc-guidance-pageNell Achebe1 fileSucceeded
05

Isolated failure

On screen

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.

What shipped
  • 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
Process

Phase by phase

  1. 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
  2. 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
  3. 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
  4. 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 & access

Held once in the account · applies to all 30 tools

InviteSearch 30 tools/NA

Seats on Workshop

2 of 2 used

3 invitations waiting

Both seats are taken, so they wait on an upgrade instead of getting a half-working login.

Upgrade

People on the plan

2 of 2 seats
PersonRoleLast active
NANell AchebeBilling and seatsOwnerToday, 14:02
TRTom ReyesRuns toolsMemberToday, 13:47

Invitations

3 waiting
PersonRoleInvitedStatus
PSPriya SandhuMember11 Sep 2026Waiting on upgrade
JWJonah WebbMember11 Sep 2026Waiting on upgrade
EMEsme MoreauMember15 Sep 2026Waiting on upgrade

Each invitation becomes a seat the moment the plan has room. Nothing to set up tool by tool.

Permissions

Set once

One set of roles for the account. Every tool reads these; none keeps its own list of who may use it.

CanOwnerMember
Run any tool in the drawer
See every run in the account
Download outputs
Change plan and billing
Invite and remove people
See invoices
Applies toall 30 tools
On screen

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 it settled

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 failsinvoice.payment_failed
  1. Card payment fails
  2. PDF merger reads it its own way
  3. CSV cleaner reads it its own way
  4. Batch resizer reads it its own way
  5. Invoice splitter reads it its own way
  6. 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.

Architecture

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.

  1. 01 · Source
    Stripe eventA subscription change arrives once, at one endpoint, and every tool reads the result.
  2. 02 · Handler
    One verified webhook handlerVerified, deduplicated and idempotent: a retried event re-reads and exits, and an unrecognised type is logged, never swallowed.
  3. 03 · State
    Entitlement record in PostgreSQLUsers, subscriptions and orders live once in the engine, so an entitlement question has exactly one answer.
  4. 04 · Registry
    Tool 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.
  5. 05 · Delivery
    Routes 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 build
Have a project?

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.