Skip to content

By Shrin4 min readDraft

CraftyCater: a second product on the same codebase

CraftyCater is free event management for caterers, built on CanteenCrowd's Go and React codebase instead of a new one: four new tables, no new dependencies, and every hard part reused, from tenant isolation to the stock ledger.

CanteenCrowd runs canteens: staff, diners, menus, purchasing, stock, requests and support, across many sites. Next to it sits a smaller job: a caterer or an event planner working alone, with their events, the crew who work them, the food, the buying, the stock, and the things they take along (chafing dishes, linen, a gas burner) and need to bring back.

That’s CraftyCater: free event management for one person per account. It’s for tracking and alerts, not for running a whole event company: what’s still to book, who’s still to pay, what’s short, what didn’t come back.

It could have been a new app. It isn’t. CraftyCater is built on the same Go backend, the same Postgres database and the same React frontend as CanteenCrowd, and this post is about why, and about what that saved.

A new product, or an edition?

Most of what CraftyCater needs already existed in CanteenCrowd: events with their plans and templates, crew double-booking checks, menus and recipes, vendors, purchase orders, stock. The plan put it at about 80%. Building on it was estimated at about 10,000 lines; a new stack, at 35,000 to 45,000. And there’d be one codebase to maintain instead of two, with CanteenCrowd’s customers getting the new features too.

So the work is split in two tracks:

  • Track A, built: the event features themselves, inside CanteenCrowd, for every customer. Crew and their pay, the event’s money, supplies, tasks and the payment schedule, the resources checklist, the production sheet and the supply list, “Mark stock used”, purchase orders from the shortfall, and alerts with “Ready for event” and “Close event” checklists.
  • Track B, next: CraftyCater as its own product. Editions, Sign in with Google, its own login page, and its own deployment.

Track A came in at 26 commits, about 4,300 lines of Go (plus 1,800 of tests) and 4,300 lines of TypeScript. It added four tables and seven migrations, and no new dependencies: neither go.mod nor the frontend’s package.json changed.

What it reused: the hard parts

New features are the easy part of a second product. The hard parts are the ones you don’t see: keeping customers apart, keeping money honest, two people not booking the same thing. Those were already built, tested and in use, and CraftyCater reused every one.

Tenant isolation, unchanged

Every customer’s data lives in the same tables, kept apart by Postgres row-level security. The four new tables (crew, the event money ledger, vendor links, stock-use links) follow the same rules as the rest:

  • row-level security enabled and forced, with the same policy on the per-transaction tenant;
  • a trigger that refuses a foreign key pointing at another tenant’s row;
  • every query run inside the same WithTenantTx helper, and every test run as the app’s own database role, so a missing policy fails a test instead of leaking data.

Nothing about tenancy had to be designed again. It had to be applied, the same way, four more times.

The append-only ledger

CanteenCrowd’s stock ledger is append-only: the app’s database role can insert rows but not update or delete them, so a mistake is corrected by a new row, and history is never rewritten.

CraftyCater’s money uses the same idea. Every client payment, crew payment and vendor payment on an event is a row in one ledger per event, and the app can only add to it. A wrong payment is reversed with a negative row, and a reversal can never take what was paid below zero. “Paid”, “owed” and the estimated profit are sums over that ledger, with the full history behind them. The links from an event to the stock it used work the same way.

The locks

A staff member can’t work two events at once: CanteenCrowd already checked that, under a Postgres advisory lock so two people saving at the same moment can’t both win. Crew members, who are new, go through the same check under the same lock. Double booking needed no new design.

The other modules’ tables, untouched

Vendor items on an event link to Purchasing’s vendor directory, and stock use links to Inventory’s movements, both with real foreign keys. But the links live on the events side, in a new schema file. The purchasing and inventory schema files didn’t change at all, so those modules, their queries and their tests carried on exactly as before.

Permissions, the frontend, the rest

  • No new permissions. CraftyCater’s features reuse the ones CanteenCrowd has: who can edit an event, who sees amounts, who can read recipes, who can record stock. The roles editor didn’t change.
  • The frontend changed in one module. Outside the events pages, only the route list and one shared button component were touched. Tables, pickers, dialogs and forms are the ones every other module uses.
  • The audit log, revisions and site scope came along for free: every new action is recorded, a stale save gets “changed by someone else” instead of overwriting, and each user sees only the sites they reach.

What comes next

  • An upgrade that moves no data. A caterer who outgrows CraftyCater should become a CanteenCrowd customer by changing one value, not by exporting and importing. One codebase makes that possible.
  • Honest limits, written down. Alerts are worked out when you open the app, with fixed thresholds; nothing is emailed yet. Resources have no catalog, so nothing knows how many chafing dishes you own. Food cost is an estimate from the last price paid. Each of these is recorded with the decision behind it, and the next step for each is known.

CraftyCater is free, and it isn’t open yet: join the waitlist to hear when it is.

Written by Shrin, who builds CanteenCrowdGitHubLinkedIn