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.
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.
Quote → multi-week build → deliver, with a client portal, milestones, and build photos the customer can follow.
Row-level multi-tenancy: every query is default-scoped to the current workshop, fail-closed, guarded by a load-bearing test suite.
Per-tenant logo, palette, and fonts on the portal and emails; payments flow through Stripe Connect straight to the shop.
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 whole design turns on one control boundary: platform admin decides which Google identities may enter which workshop. Past that gate, everything else follows.