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_idcolumn 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.
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.
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.