Engineering
One deployment, every gym
How and why we replaced branch-per-customer with multi-tenant subdomain routing. We are an early-stage product — live at one Mumbai gym — and that is exactly why we fixed this now, before the old architecture could calcify.
Where we started
TraqGym's first deployments were one Vercel project, one git branch, and one database per gym. It was the fastest way to get our first customer live, and it worked — but it does not scale past a handful of gyms:
- Deploy fan-out. Every bug fix had to be merged into every customer branch and deployed N times. At 2 gyms that is annoying; at 50 it is a full-time job.
- No shared upgrades. New features and schema migrations had to be replayed per branch and per database, so customers drifted onto different versions of the product.
- Config sprawl. N projects meant N sets of environment variables, domains, and webhooks to keep in sync by hand.
BEFORE: branch-per-customer gym-a branch ──> Vercel project A ──> database A gym-b branch ──> Vercel project B ──> database B gym-c branch ──> Vercel project C ──> database C one fix = N merges + N deploys + N migrations
Scroll sideways for the full diagram →
Where we are now
Today there is a single deployment. Middleware reads the Host header, extracts the gym's subdomain, and rewrites the request to that gym's workspace — the marketing site and the tenant workspaces are the same app. Data isolation moves to where it belongs: the database layer, with every row scoped to its gym and every query filtered by tenant.
AFTER: one deployment + subdomain routing
gym-a.traqgym.com ────────┐
gym-b.traqgym.com ────────┼──> middleware ──> one Next.js app
{your-gym}.traqgym.com ───┘ Host header │
rewritten to ▼
/gym/{slug} one database
(rows isolated per gym)
one fix = one merge + one deploy + one migrationScroll sideways for the full diagram →
Honest status: the *.traqgym.com wildcard still points at the legacy per-gym setup, so tenant subdomains do not resolve against this deployment yet. Until the cutover below completes, workspaces are served from this deployment at /gym/{slug} (see the demo at /gym/demo), and we activate each gym's subdomain during onboarding.
- One codebase, one deploy. Every gym is on the same version, always.
- Isolation at the data layer. Tenant separation is enforced in the database — per-gym scoping on every table and every query — not by physically separate infrastructure.
- Instant onboarding. A new gym is a row and a subdomain, not a new branch, project, and database.
Migration path for our live gym
Our live gym currently runs on the old branch-per-customer setup, and it stays there until cutover is proven. The plan, in order: export the gym's database and import it into the shared database under the gym's tenant scope; run the subdomain workspace read-only against the migrated copy and reconcile it against the live system; then point the gym's subdomain at the shared deployment and retire its branch, with the old deployment kept warm as a rollback until the cutover has survived a full billing cycle.