Shoptigo: one codebase, many online stores
A multi-tenant ordering platform for local delivery and wholesale businesses (butchers, bakeries, farm shops, distributors), built on the open-source Vendure commerce framework. One deployment runs many shops, and adding a shop never requires a code change or a deploy.
| Role | Architect and operator. AI coding agents wrote the code under my direction. |
|---|---|
| Timeline | Aug 17 – Oct 8, 2026 · 237 commits · about 62,000 lines |
| Backend | Vendure 3 (NestJS, TypeORM, GraphQL), 11 custom plugins, PostgreSQL 17 |
| Storefront | Remix, Tailwind, GraphQL codegen, deployed as a Cloudflare Worker |
| Infrastructure | Docker and Coolify on a VPS, Cloudflare for SaaS custom domains, R2 storage, encrypted point-in-time backups, GitHub Actions CI |
| Status | First shop live: a Cincinnati ice-cream distributor's wholesale ordering site |
The first shop, live
A wholesale distributor's ordering site: customers order by the case from a fast order list, browse by category with filters, and check out with saved cards. Everything here (name, colors, layout, categories) is configuration the shop controls from its dashboard. None of it is in the code.





The core rule
"Would shop number two need to edit this file? If yes, it's a setting."
Nothing about a specific shop lives in the code: not its colors, address, hours, delivery or payment methods. All of it is configuration that the shop owner controls from their dashboard. Unset settings fall back to sensible defaults, so adding a new setting can never change an existing shop's behavior.
What a shop gets
- A branded storefront with two site styles, five homepage layouts, generated color palettes, and an Appearance screen with live preview
- A store dashboard reworded in shop-owner language instead of developer terms
- Delivery limited by postcode, in-person collection, cash and counter sales
- Trade accounts with tiered wholesale pricing and enrollment codes
- Stripe payments with keys entered per shop, plus cash payments
- Their own domain, connected from the dashboard. No deploy, no ticket.
Hard problems solved
Retrofitting tenant isolation onto a framework that wasn't built for it
Vendure's per-channel scoping is opt-in, so many operations weren't isolated by default. I had agents sweep the system for cross-shop leaks, and the sweeps found real ones: refunding another shop's payment, reading another shop's channel, copying another shop's catalog, customer groups and Stripe customer records crossing shops. Each fix shipped with an automated probe, a script that boots a throwaway store and tries the attack.
About 30 tenancy probes plus 27 unit test files run in GitHub Actions on every push. A shop-owner role test fails the build if a forbidden permission ever gets granted.
Onboarding a shop with zero deploys
Custom domains run through Cloudflare for SaaS, a wildcard Worker route works out which shop a visitor wants from the hostname, and cache purges look up the right zone at runtime. Two earlier designs that needed a config edit per shop were found and removed. The rule is now written down: if onboarding a shop needs someone to edit a file, it's wrong.
A 6.5 GB bandwidth bill nobody asked for
The hosting bill showed far more traffic than the shops could explain. I traced it to the background job queue polling a remote database nonstop. The fix replaced polling with Postgres notifications (LISTEN/NOTIFY): the queue sleeps until there's work. A benchmark script proves the difference.
Owning the database
I moved the database from a hosted provider to self-hosted Postgres 17 with WAL-G: continuous, encrypted, point-in-time backups to Cloudflare R2. The cutover and rollback are documented, and there's an end-to-end test of backup and restore.
Turning an outage into a guardrail
A newly required server secret took every route down with errors. Now a smoke test builds and boots the storefront and fails on any server error before anything ships.
How the work was directed
- A rules file for the AI agents: the template-versus-shop test, the no-deploy domain rule, "branch on fields, never on names," and "verify against the running system and say plainly what you couldn't verify."
- Decision records that explain the reasoning, not the code: tenant isolation (about 800 lines), shipping and collection, counter sales, and upgrading Vendure safely.
- A list of every place the project depends on Vendure's internals, so framework upgrades don't break silently.