Building a multi-tenant Link-in-Bio product can quickly lead to complex hosting setups. Here is how we managed multiple tiers on one shared backend.
By Rehan Idrisi · · Updated · 5 min read
When launching a SaaS product, it is tempting to separate user tiers by hosting separate code deployments or spawning dedicated databases. That does provide isolation. It also multiplies maintenance overhead by ten, because every migration, every security patch and every feature now has to land N times instead of once.
While building a link-in-bio platform for local vendors, we opted for a single shared backend codebase. Access to premium tools such as customized designs or native payment collections is feature-gated at the logical level, not the infrastructure level. This is the breakdown of how to make multi-tenancy boring and clean.
Instead of isolating databases, we used a shared database model where every table includes a tenant ID or user owner ID. A global middleware layer automatically scopes SQL queries to the current authenticated tenant's ID.
The leak gets prevented at the application entrypoint. One vendor cannot query or modify another vendor's layout or payment credentials, because a query never reaches the database unscoped.
The permission-check system is lightweight. A user profile payload returned from the database carries a 'tier' field: 'free', 'pro', 'enterprise'.
Every API route request runs a middleware check against that tier. A free user who attempts to enable custom CSS styling or configure a payment gateway is rejected immediately with an HTTP 403 Forbidden, before any handler runs.
function checkFeatureAccess(requiredTier) {
return (req, res, next) => {
const userTier = req.user.tier; // e.g. 'free'
const tiers = { free: 0, pro: 1, enterprise: 2 };
if (tiers[userTier] < tiers[requiredTier]) {
return res.status(403).json({
error: 'Feature locked. Upgrade required.'
});
}
next();
};
}Gating covers more than an on/off switch, because quotas matter just as much. A free user might be capped at 5 links while a pro user gets unlimited links. We implement that by running aggregation counts before inserting records.
The count query runs inside the active write transaction. That is what guarantees a user cannot slip past their tier limit by firing concurrent API requests.
A subscription event fires, say a user paying through the payment gateway, and we receive a secure webhook payload on the server. The webhook updates the tenant's tier column inside the users table.
Because the feature gates read current database state on every request, the tenant gains access the moment that column changes. No redeployment. No container restarts.
Server-side gating is the security boundary. A backend-only check still makes for a confusing product when the UI doesn't reflect it, and a free-tier user should not be able to fill in a custom-domain field only to discover after submitting that it was locked the whole time. The frontend reads the same tier field and shows locked features as visibly disabled with an upgrade prompt. That is a UX affordance and nothing more. Every one of those routes is re-checked server-side regardless of what the client displayed, since a client-side-only lock is trivial to bypass with a direct API call.
The failure mode worth designing against is not a clever attack. It is an ordinary bug where a new query forgets the tenant-scoping filter and returns rows across tenants. We caught this class of bug in two layers. Code review carries a lint-style check looking specifically for raw queries against tenant-scoped tables that bypass the scoping middleware. An integration test suite then creates two tenants with overlapping data, on purpose, and asserts that every read endpoint returns only the requesting tenant's rows. That second layer has caught a scoping regression before it reached production more than once.
One shared backend means one tenant's unusual usage pattern can slow things down for everyone else. A sudden spike in API calls, a very large export request: with fully isolated infrastructure that risk simply doesn't exist, so it is a genuine trade-off to account for. We mitigate it with per-tenant rate limits on expensive operations such as exports and bulk updates, and by moving heavy jobs onto a background queue instead of the request path, so no single tenant's burst of activity blocks the API workers serving everyone else.
A SaaS with 10 infrastructure instances is 10 times harder to keep secure. Keep it singular and gate access at the logical layer.
Keep one shared database and codebase, scope every query by tenant ID through a global middleware, and check the user's subscription tier at the API-route level instead of deploying separate environments per plan. Webhooks update the tier field on payment events, so upgrades take effect immediately with zero redeploys.
Clean, format, parse, and validate JSON inputs with instant syntax lint warnings and code export.
Decode, verify, parse, and edit JWT token components including headers, payloads, and signatures.
Generate SHA-1, SHA-256, SHA-512, and MD5 cryptographic hash values for strings in real time.