Platform project · 2026

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.

RoleArchitect and operator. AI coding agents wrote the code under my direction.
TimelineAug 17 – Oct 8, 2026 · 237 commits · about 62,000 lines
BackendVendure 3 (NestJS, TypeORM, GraphQL), 11 custom plugins, PostgreSQL 17
StorefrontRemix, Tailwind, GraphQL codegen, deployed as a Cloudflare Worker
InfrastructureDocker and Coolify on a VPS, Cloudflare for SaaS custom domains, R2 storage, encrypted point-in-time backups, GitHub Actions CI
StatusFirst 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.

polarbearcincy.com
Wholesale order list
The order list: wholesale customers add cases straight from one screen
Order list on a phone
Order list
Category on a phone
Category with filters
Product page on a phone
Product page
polarbearcincy.com
Category page with filters
Category pages with flavor and size filters

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

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