Multi-tenant SaaS for $30/mo: the actual architecture
Every "build a SaaS" tutorial reaches for Kubernetes, managed Postgres, and three cloud services before the first user signs up. You don't need that. Here's a multi-tenant setup that runs 50+ tenants on subdomains, on one $30/mo VPS.
The requirement
White-label partnership program. Each partner gets a storefront at partner-{slug}.guardlabs.online with our catalog, their referral IDs baked in. New partner → automatic provisioning after payment webhook.
Phase 1 (0-50 tenants): wildcard SSL + nginx + Flask
Wildcard SSL via Let's Encrypt — one cert covers *.guardlabs.online:
certbot certonly --manual --preferred-challenges dns \
-d "*.guardlabs.online" -d "guardlabs.online"
(DNS-01 challenge — add the TXT record your DNS provider, certbot validates.)
nginx server block routing by $host:
server {
listen 443 ssl;
server_name ~^partner-(?<partner_slug>.+)\.guardlabs\.online$;
ssl_certificate /etc/letsencrypt/live/guardlabs.online/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/guardlabs.online/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Partner-Slug $partner_slug;
}
}
Flask reads the header, scopes everything to that partner:
@app.before_request
def load_partner():
slug = request.headers.get("X-Partner-Slug")
if slug:
g.partner = db.execute(
"SELECT * FROM partners WHERE slug=?", (slug,)
).fetchone()
if not g.partner:
abort(404)
@app.route("/")
def storefront():
products = get_catalog() # global catalog
return render_template("store.html",
products=products,
ref_id=g.partner["ref_id"], # injected into every buy link
partner_brand=g.partner["brand_name"])
Provisioning on webhook — when Whop sends "new subscription":
@app.route("/webhook/whop", methods=["POST"])
def whop_webhook():
event = verify_and_parse(request)
if event["type"] == "subscription.created":
slug = slugify(event["user"]["username"])
# 1. DB row
db.execute("INSERT INTO partners (slug, ref_id, ...) VALUES (...)")
# 2. nginx — no per-partner config needed! Regex server_name catches it.
# 3. Done. partner-{slug}.guardlabs.online works immediately.
return "", 200
That's the trick: the regex server_name means zero new nginx config per partner. Wildcard SSL means zero new certs. The DB row is the only write.
Capacity: one VPS, monolith Flask, SQLite. ~50 partners with 10 concurrent users each = 500 concurrent. A $30/mo Hetzner CCX handles that without breathing hard.
Phase 2 (50-200 tenants): per-tenant SQLite
Move from shared DB to /data/partners/{partner_id}/store.db. Flask switches connection by X-Partner-Slug. ~16 hours of refactor. SQLite holds 10k+ rows per file fine; 200 partners × 10k = 2M rows total, no problem.
Phase 3 (200-500+): PostgreSQL schema-per-tenant
Now you migrate. One Postgres, schema per partner. 40 hours. Now you might want Kubernetes. Not before.
The point
I provisioned this whole thing in about 8 hours. Total infra budget for 12 months: under $5K. The "you need microservices and managed everything" advice is for companies with funding and a platform team. For a solo founder: nginx regex + wildcard SSL + a Flask before_request hook gets you to 50 paying tenants.
Try it
The partnership it powers: 7-day free trial, no card, $29/mo. guardlabs.online/partner. And the bot the whole thing grew out of: Phantom paper-trader (384 trades, 57% win-rate, public).
Question
What's your "we over-engineered this" story? When did you reach for Kubernetes before you needed it?
Code and full launch log in public. Following along.
📥 Free chapter — 20 no-budget growth tactics
This launch log runs on a playbook. If you want the actual tactics — Google-ecosystem hacks, trend-jacking, the HARO authority play — grab two free sections of the Blueprint. No PDF wall, no login: it opens in your browser. Real numbers, real code, no fluff.
Top comments (0)