Skip to content

03 / Anitra Murphy · US · 2020

A database for every tenant

White-label agency platform with a dedicated database per tenant, provisioned automatically at signup.

Role
Solo Developer & Architect
Timeline
8 months, alongside FlexSited
Type
Multi-tenant agency SaaS
Stack
Laravel 8 · Vue 2 · stancl/tenancy · Stripe Connect · Pusher · MySQL · Tailwind CSS

At a glance

  • White-label agency SaaS with full database-per-tenant isolation: every signup provisions, migrates, and seeds its own MySQL database automatically.
  • Stripe Connect as payment facilitator: tenant agencies process their own clients' billing under their own Stripe accounts.
  • Tenant-scoped everything: WebSocket channels, queue jobs, file storage, cache, all verified at the infrastructure layer.
  • Built solo in 2020, in parallel with a second platform (FlexSited), inside eight months. Fully launched; later shut down by the owner.

01 / Context

One platform to replace an agency's tool pile

Agencies run on a pile of subscriptions: Asana for projects, Slack for chat, Stripe for billing, Freshdesk for support. Visionerro's pitch was consolidation: a white-labeled platform where each agency gets its own subdomain, brings its own clients, and runs delivery, communication, and billing in one place.

Anitra Murphy hired me as the sole developer. "White-label multi-tenant" was the requirement that shaped everything else. Agencies would trust the platform with their client relationships only if isolation was real, not cosmetic.

02 / Constraints

Eight months, two platforms, one developer

  • Hard isolation as a feature. Agencies' client data, files, and payments must be provably separate. Shared-table tenancy with a tenant_id column wasn't going to sell.
  • Split attention. FlexSited was being built for the same client in the same eight months. Every architectural shortcut on one platform stole time from the other.
  • Payments without becoming a processor. The platform had to facilitate agency billing without ever holding agency money.

03 / Provisioning

Signup that builds infrastructure

I chose stancl/tenancy with database-per-tenant isolation: each agency lives in its own MySQL database, resolved by subdomain. That makes signup an infrastructure event: four jobs run as an atomic pipeline, and a new agency's workspace exists before their welcome email lands.

JobPipeline, atomic: a failure at any step must be recoverableSignupagency chooses subdomainCreateDatabasefresh MySQL dbMigrateDatabasefull schemaSeedDatabasedefaults + plansCreateTenantAdminowner accountLive{agency}.domaincentral dbtenants · domains · plans · billingtenant db, isolatedprojects · chats · tickets · files · 20+ tables
Fig. 01: signup as a JobPipeline. Ember steps create real infrastructure; a failure anywhere must be recoverable.

The pipeline's honesty problem is partial failure: a database created but not migrated is worse than no database at all. Building recoverability into each step (idempotent creation, resumable migration, seed-once semantics) was most of the actual work, and none of it is visible in a demo.

04 / Runtime isolation

Every request, already in the right universe

Isolation only counts if it holds on every path into the system: HTTP, WebSockets, queues, files, cache. The HTTP path is the easy one: middleware resolves the tenant from the subdomain and switches the database connection before any application code runs.

http request{agency}.visionerro.comany tenant subdomainInitializeTenancyByDomainresolve tenant, switch contextDatabaseTenancyBootstrapperCacheTenancyBootstrapperFilesystemTenancyBootstrapperQueueTenancyBootstrappertenant db+ scoped cache,files, queueswebsocketPusher channel authx-tenant headerBroadcastingTenancyInitializerinitialize tenancy before authorizingno tenant match → no channel
Fig. 02: tenancy initialization on the HTTP and WebSocket paths. The bootstrappers swap infrastructure, not queries.

The paths that bite are the asynchronous ones. Queue jobs serialize on one tenant's behalf and wake up in a worker that serves everyone. Tenant context rides inside the job payload and re-initializes on wake. WebSocket channel authorization reads an x-tenant header and initializes tenancy before deciding whether the subscriber belongs there. Files get tenant-suffixed storage paths. Miss any one of these and "isolated" becomes a marketing word.

05 / Payments

Stripe Connect without touching the money

Each tenant admin connects their own Stripe account; the platform acts as facilitator, not processor. At runtime, tenant Stripe credentials map into Laravel Cashier's configuration through a storage-to-config bridge, which means the same billing code serves every agency, each against their own Stripe account, with per-tenant webhook handling to match.

This model kept agencies' money in agencies' accounts and kept the platform out of money-transmitter territory, the right shape legally and the right shape for trust.

06 / Tradeoffs

Elegant isolation, operational weight

Database-per-tenant: defend, with eyes open. The isolation story is airtight and agencies' data can even be exported wholesale. But every schema change is a fleet migration, CLI commands need explicit tenant context, and a failed migration on tenant #37 is a very specific kind of morning. I'd choose it again for this product, and shared-schema for most others.

Velocity over test depth: the eight-month tax. Two platforms in eight months meant manual testing carried more weight than it should. The provisioning pipeline got the care; plenty of feature surface shipped on discipline and luck.

database per tenant, isolated
user roles with granular permissions
in-app notification types
platforms shipped in 8 months

07 / Outcome

Built, launched, and switched off

Visionerro shipped complete: projects with milestone approval workflows, three kinds of real-time chat, a service marketplace, two-tier support tickets, time tracking, granular per-staff permissions, plan-based storage enforcement, 25 notification types. It launched. It worked.

And then the owner shut it down. A business decision, made after development was complete. I include that deliberately because a portfolio of only victories has the interesting parts removed. The engineering held; the market bet did not. I learned more from this project than from several that "succeeded."

08 / Lessons

What database-per-tenant taught me

  • Tenancy is an infrastructure property, not a query filter. If isolation isn't enforced below application code, it isn't enforced.
  • Provisioning pipelines are distributed transactions in disguise. Design for partial failure first, happy path second.
  • Async paths (queues, WebSockets, scheduled tasks) are where tenant context dies quietly. Test those boundaries hardest.
  • Payment facilitation via Connect is mostly webhook discipline. Per-tenant credentials are easy; per-tenant reconciliation is the job.