DEV Community

Cover image for There Are 5 Ways to Do Multi-Tenancy. I Automated the Hardest One.
Jonathan Pitter
Jonathan Pitter

Posted on

There Are 5 Ways to Do Multi-Tenancy. I Automated the Hardest One.

If you've ever built a SaaS product that serves multiple customers, you've stared at this decision:

How do I keep Customer A's data away from Customer B?

The answer seems simple until you actually try to implement it.

Then you discover that every approach on the multi-tenancy spectrum is a different flavor of pain.

The Multi-Tenancy Spectrum (And Why Every Option Hurts)

Level 1: Shared Database, Shared Schema

The most common starting point. Everyone's data lives in the same tables, separated by a tenant_id column.

-- Every query needs this.
SELECT * FROM orders WHERE id = $1 AND tenant_id = $2;

-- Forget the tenant_id filter once and you have a data breach.
SELECT * FROM orders WHERE id = $1; 
Enter fullscreen mode Exit fullscreen mode

It's cheap and simple, until it isn't. A single missed WHERE clause leaks data across tenants. Junior devs joining the team won't always remember. Your ORM might not enforce it. And PostgreSQL itself isn't immune CVE-2024-10976, disclosed in late 2024, showed that row-level security policies could be bypassed when queries were reused across different roles, potentially letting one tenant's query return another tenant's rows.

Then there's the noisy neighbor problem. One tenant runs a massive data import and suddenly every other tenant's dashboard grinds to a halt. You can't set per-tenant resource limits because everyone shares the same compute and storage.

Level 2: Shared Database, Separate Schemas

More isolation as each tenant gets their own schema inside one database. tenant_a.orders, tenant_b.orders.
The problem shifts from simply remembering the where clause to making sure you can coordinate schema migrations across 'N' number of schemas with zero downtime. At 10 tenants, that's manageable. At 500, you've built a migration orchestration system. And tenants still share the underlying database resources, so noisy neighbors are still a thing.

Multiple sources I reviewed while building Staxa recommend avoiding this approach entirely. It combines the operational complexity of separate databases with the resource-sharing downsides of a shared one. You can check this out

Level 3: Database Per Tenant

Now we're getting to real isolation. Each tenant has their own database instance, Backup and restores are on a per-tenant level. A failure in one tenant's database doesn't cascade to others.

But you're now running migrations across 'N' separate databases. Schema version drift becomes a real risk. Your CI/CD pipeline needs to understand which tenants are on which version, infrastructure costs scale linearly and you still haven't isolated the application layer. Your API server is shared, your compute is shared and your networking is shared.

Level 4: Namespace or Container Isolation

Pushing a step further, each tenant runs in their own Kubernetes namespace or container group, with their own database. Now you've isolated compute, storage, and networking.

This is where things get architecturally correct but operationally brutal. You need to automate namespace creation, deploy applications into each one, configure ingress routing per tenant, provision and inject database credentials, set up SSL certificates, manage resource quotas, configure network policies, and wire up health checks, for every single tenant.

Most teams that attempt this build a custom control plane. That control plane becomes its own product to maintain.

Level 5: Fully Siloed, Dedicated Infrastructure Per Customer

The golden standard, each customer gets their own everything

  • Application
  • Database
  • Subdomain
  • SSL
  • Network isolation

  • No shared resources.

  • No chance of cross-tenant data leakage.

  • No noisy neighbor issues.

Enterprise customers in regulated industries demand this, Government contracts require it and even B2B SaaS customers increasingly ask for it as a premium tier.

The tradeoff? It's the hardest approach to operationalize by far. Without automation, provisioning a new customer environment typically takes two to six weeks, accounting for infrastructure setup, security configuration, networking, and testing. Supporting one or two dedicated deployments manually is feasible. However supporting ten or more without a purpose-built system requires significant dedicated engineering investment.

This is the problem I decided to solve.

Staxa: Full Tenant Isolation in 60 Seconds

Staxa is an API-first platform that automates Level 5 multi-tenancy. One API call, and ~60 seconds later your customer has:

  • Their own application container running their framework
  • A dedicated PostgreSQL or MySQL database
  • A unique subdomain with automatic SSL via Let's Encrypt
  • Full Kubernetes namespace isolation: network policies, resource quotas, RBAC
  • Environment variables encrypted at rest with AES-256-GCM
curl -X POST https://api.staxa.dev/api/v1/tenants \
  -H "Authorization: Bearer sk_live_xxx" \
  -d '{
    "name": "acme-corp",
    "source": {
      "type": "github",
      "repo_url": "https://github.com/acme/app"
    },
    "database": { "engine": "postgres" }
  }'
Enter fullscreen mode Exit fullscreen mode

That's it. Hook this into a Stripe webhook and your SaaS provisions isolated customer environments automatically on payment. No Terraform configs, No DevOps tickets, No two-week onboarding process.
There's also a visual dashboard with a deployment wizard for teams that prefer point-and-click over API calls. Both interfaces hit the same backend, the dashboard just builds the same JSON payload through a multi-step UI flow.

How It Works Under the Hood

The entire platform is a single Go binary (staxad) running on a k3s Kubernetes cluster. When a deploy request comes in, it moves through a 7-stage pipeline:

  1. Provision → Create the Kubernetes namespace, RBAC rules, ResourceQuotas, and NetworkPolicies for the tenant.

  2. Detect → The framework detection system identifies which frameworks your project uses from file markers and dependency files. 26 frameworks supported, deterministic file-based detection with a two-tier confidence system. No external API calls, zero latency overhead, zero external dependencies.

  3. Generate → This one came from a friend but if you have no Dockerfile in your project? Then auto-generate a Dockerfile using Go text/template with framework-specific configs pulled from the database. Ports, health check paths, build commands, start commands, all operator-tunable without code changes.

  4. Build → Buildah runs as a Kubernetes job, builds the image, pushes to the internal registry. No Docker-in-Docker, no privileged containers.

  5. Deploy → Kubernetes Deployment created with resource limits, health checks, and environment variables. Database provisioned in the same namespace if requested.

  6. Route → Traefik IngressRoutes for the tenant's subdomain. cert-manager handles SSL automatically.

  7. Health Check → Poll the health endpoint until the app responds, then mark as ready.

The caller gets real-time progress via Server-Sent Events throughout all seven stages. The dashboard shows a live progress bar; an API consumer streams the same events programmatically.

Hetzner CAX21 (~€8/month) — k3s single-node cluster

staxa-system namespace

  • staxad — Go API, single binary, 49 endpoints
  • PostgreSQL — platform database
  • Redis — job queue + SSE pub/sub
  • Next.js — dashboard + deployment wizard

tenant-acme namespace

  • app container (Next.js)
  • dedicated postgres

tenant-widgetco namespace

  • frontend service
  • backend service
  • dedicated postgres

Traefik — ingress controller, auto-SSL via cert-manager, *.staxa.dev routing

A tenant can have multiple services (frontend + backend) sharing a namespace, internal networking, and database credentials. Service URL injection (STAXA_SERVICE_{NAME}_URL) handles cross-service discovery automatically.

Why Go, Why Kubernetes, Why a $10 Server?

Go because infrastructure tooling lives in Go. Docker, Kubernetes, Terraform, Traefik, Caddy, all Go and I can't help trying new things :).

The client-go library for Kubernetes is first-class. Goroutines handle parallel tenant deployments naturally. And a single compiled binary means the entire control plane is one scp away from running on any server.

Kubernetes (k3s) because the abstractions map perfectly to the multi-tenancy problem.

  • Namespaces: tenant boundaries.
  • ResourceQuotas: noisy neighbor prevention.
  • NetworkPolicies: traffic isolation.
  • Deployments: rolling updates and rollbacks for free.

I'm not bolting isolation onto a system that wasn't designed for it, I'm using a system where isolation is a first-class primitive.

Hetzner server because the old version of this platform ran on AWS and cost $200+/month with zero tenants deployed, literally just from developing, I almost gave up entirely after that pain. It's the same architecture, databases in containers instead of managed services, commodity ARM compute. The entire platform and 10-15 demo tenants run comfortably on one box.

Design Decisions That Matter

No magic numbers: Every tunable value — resource limits, port ranges, framework templates, detection rules; lives in a platform_config database table. An operator can change any default setting without redeploying the whole platform. This was a hard rule from day one.

No external AI for framework detection: I evaluated using an LLM API to detect project frameworks and rejected it since that would be easier. However the latency, cost, and external dependency were all liabilities. Deterministic file-based detection with parallel file fetching worked out faster, cheaper (the cost would scale with projects deployed), and has no external dependencies.

49 API endpoints across 12 groups: Tenants, deployments, services, domains, environment variables, network rules, container registry credentials, templates, and more.

Dual auth: Clerk JWTs for dashboard users, API keys with prefix lookup for programmatic access.

Multi-service tenants: Staxa models tenants as isolation boundaries that can contain multiple services because real applications have frontends and backends that need to talk to each other.

Who This Is For

  • SaaS founders whose enterprise customers are asking for dedicated environments. Instead of months of infrastructure work, you integrate one API call into your onboarding flow, that's it.

  • Agencies who need isolated environments per client, each with their own domain and SSL without one client's traffic spike affecting another.

  • Platform engineers who need on-demand isolated environments for dev teams or staging, without giving everyone kubectl access.

The Numbers

Metric Value
Infrastructure cost ~€8/month
Time to provision a fully isolated tenant ~60 seconds
Provider-facing API endpoints 49
Frameworks detected 26
Auto-generated Dockerfile templates 19
External SaaS dependencies 2 (Clerk for auth, Cloudflare for DNS)
V1 AWS cost (zero tenants) $200+/month

Try It
Staxa is in early access and I'm working directly with early adopters.

👉 Play around and join the waitlist if interested at staxa.dev

Top comments (0)