case file 07 · a multi-tenant shop OS

OurWorkshop.

A white-label back office for small custom-maker shops — woodworkers, framers, sign shops — to run the one arc they all share: lead → quote → multi-week build → deliver, with the client watching the whole way.

in active development
§ what it is

The software the client watches, not just the one you close in.

Think of a husband-and-wife shop that takes custom inquiries, sends a quote, builds over several weeks, and keeps the customer excited with progress photos. A CRM's job ends at the deal. This one keeps going — past the close, through the build, all the way to delivery — and brings the client along with a portal, milestones, and photos.

It grew out of a real single-tenant app I'd built for one maker (mh-handcraft). OurWorkshop is that domain generalized into a white-label platform: every shop runs its business at theirshop.ourworkshop.io, with its own branding, catalog, and client list.

The deliberate stance: we are not a website. A shop's public presence stays on the shop's own domain. OurWorkshop stays out of the design business and feeds that site two API-shaped things — a catalog feed to read and a lead-intake endpoint (plus a drop-in widget) to write — then owns everything after the lead lands.

Clients never get a login. They reach their project through a portal token + PIN and see only what the shop has chosen to share. Shop owners self-manage their team; I (platform admin) gate which Google identities may enter each workshop.

01

Past the close

Quote → multi-week build → deliver, with a client portal, milestones, and build photos the customer can follow.

02

One house, many shops

Row-level multi-tenancy: every query is default-scoped to the current workshop, fail-closed, guarded by a load-bearing test suite.

03

Their brand, their money

Per-tenant logo, palette, and fonts on the portal and emails; payments flow through Stripe Connect straight to the shop.

§ my musings

Generalize in place. Don't rewrite.

The domain was already built and battle-tested for one shop. The interesting problem wasn't the features — it was turning “one” into “N” without breaking the one.

The tempting move is a clean-slate rewrite for multi-tenancy. I did the opposite: copy the working single-tenant app and change only what tenancy and white-labelling require, proving parity as tenant #1 before any new feature. It's less glamorous and far less risky.

The load-bearing decision is isolation. I moved from SQLite-per-tenant to one shared Postgres with row-level workshop_id scoping — every model fail-closed to the current tenant, so an unbound query returns no rows rather than everyone's. That convention only holds because a test suite treats "tenant A can see tenant B" as a P0. The safety isn't the code; it's the test that dares the code to leak.

§ the architecture

Three audiences, one boundary.

⚙ how it's wired theirshop.ourworkshop.io

The whole design turns on one control boundary: platform admin decides which Google identities may enter which workshop. Past that gate, everything else follows.

  1. Platform admin (me) creates workshops, allow-lists Google accounts, can impersonate, and bills.
  2. Workshop members (shop staff) run projects, quotes, and the client portal — no billing, no platform access.
  3. Clients get no account at all — a portal token + PIN scopes them to exactly what's shared.
  4. The shop's own public site consumes a catalog feed and posts leads back in; OurWorkshop owns the whole journey after.
§ under the hood
Rails 8.1 · Hotwire Propshaft + importmap · Stimulus PostgreSQL · row-level tenancy pgvector Google OAuth · per-shop allow-list Stripe Connect Fly.io
← prev case fileHushbin back to the top →Weave