The Challenge
I've worked across multiple multi-tenant SaaS engagements — including a 6-month tenure on a multi-brand QSR HR platform deployed across KFC, Five Guys, and Burger King franchise networks. The core challenge is universal: build one codebase that serves multiple distinct customer organisations, keep their data completely isolated, and scale to thousands of locations without operations costs ballooning. This piece is a field guide to the patterns that survive contact with production.
Multi-Tenancy: Database Design Decisions
One of the first critical decisions was choosing the multi-tenancy strategy. There are three main approaches:
- Database per tenant - Complete isolation but expensive and hard to maintain
- Schema per tenant - Good balance but still management overhead
- Shared database with tenant ID - Most scalable, requires careful design
For EasyRHIS, I chose the shared database with tenant ID approach using PostgreSQL. Every table includes a tenant_id column, and I implemented Row-Level Security (RLS) policies to enforce data isolation at the database level.
Implementation Strategy
The key was making tenant isolation invisible to application code. I created a custom middleware that:
- Extracts tenant information from JWT tokens
- Sets session context variables in PostgreSQL
- Applies RLS policies automatically on every query
This meant developers could write queries without worrying about tenant filtering — the database handles it automatically. A query like SELECT * FROM employeesonly returns employees for the authenticated tenant.
Authentication & Authorization Architecture
Multi-tenant authentication adds complexity. I needed to support:
- Restaurant chain administrators (full access to their locations)
- Store managers (access to specific locations)
- Employees (limited self-service access)
I implemented a hierarchical role-based access control (RBAC) system using Supabase Auth with custom claims in JWT tokens:
{
"tenant_id": "uuid-here",
"role": "store_manager",
"locations": ["location-1", "location-2"],
"permissions": ["read:employees", "write:schedules"]
}Scaling Considerations
As the platform grew to serve multiple restaurant chains with hundreds of locations, several challenges emerged:
1. Query Performance
Adding tenant_id to every query can hurt performance if not properly indexed. I created composite indexes with tenant_id as the first column:
CREATE INDEX idx_employees_tenant_location
ON employees(tenant_id, location_id, created_at DESC);2. Database Connection Pooling
With multiple tenants hitting the database simultaneously, connection pooling became critical. I used Supabase's built-in connection pooling with PgBouncer in transaction mode to handle thousands of concurrent requests.
3. Background Jobs
Scheduled tasks like payroll processing and report generation needed to run per tenant. I implemented a job queue system that:
- Processes jobs tenant-by-tenant to maintain isolation
- Implements rate limiting per tenant to prevent resource monopolization
- Uses separate queues for high-priority vs. batch operations
Frontend Multi-Tenancy
The frontend (Next.js 14 + TypeScript) needed to handle tenant switching gracefully. I implemented:
Tenant Context Provider
// src/contexts/TenantContext.tsx
const TenantContext = createContext<TenantContextType>(null!);
export function TenantProvider({ children }: { children: ReactNode }) {
const [tenant, setTenant] = useState<Tenant | null>(null);
useEffect(() => {
// Load tenant from JWT on mount
const loadTenant = async () => {
const session = await supabase.auth.getSession();
if (session?.data?.session) {
setTenant(session.data.session.user.user_metadata.tenant);
}
};
loadTenant();
}, []);
return (
<TenantContext.Provider value={{ tenant }}>
{children}
</TenantContext.Provider>
);
}Tenant-Specific Branding
Each restaurant chain wanted their own branding. I implemented a theming system that:
- Loads tenant-specific colors, logos, and fonts from the database
- Applies CSS variables dynamically on tenant switch
- Caches tenant configuration in localStorage for performance
Security Best Practices
Multi-tenancy security is paramount. Here's what I learned:
1. Never Trust Client-Side Tenant IDs
Always extract tenant information from server-side JWT tokens, never from client requests. Malicious users could forge tenant IDs to access other tenants' data.
2. Audit Logging
I implemented comprehensive audit logs that track:
- Who accessed what data
- When they accessed it
- What changes were made
- Their tenant context at the time
3. Rate Limiting Per Tenant
To prevent one tenant from affecting others, I implemented tenant-aware rate limiting using Redis:
// Example rate limit: 100 requests per minute per tenant
const key = `rate_limit:${tenantId}:${Math.floor(Date.now() / 60000)}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, 60);
if (count > 100) throw new Error('Rate limit exceeded');Deployment & Infrastructure
EasyRHIS is deployed on Vercel with Supabase as the backend. Key infrastructure decisions:
- Edge Functions for tenant-specific API endpoints that run close to users
- Database Replicas for read-heavy operations like reporting
- CDN Caching with tenant-aware cache keys for static assets
Lessons Learned
1. Start with Multi-Tenancy from Day One
Retrofitting multi-tenancy into an existing application is extremely difficult. Build it into the architecture from the beginning, even if you only have one tenant initially.
2. Test Tenant Isolation Rigorously
Write integration tests that verify data isolation. I created test suites that:
- Authenticate as different tenants
- Attempt to access other tenants' data
- Verify that operations only affect the current tenant
3. Monitor Per-Tenant Metrics
Build observability into your multi-tenant system. Track metrics like:
- Request volume per tenant
- Database query performance per tenant
- Error rates per tenant
- Storage usage per tenant
This helps identify problematic tenants and optimize accordingly.
4. Plan for Tenant Migration
Sometimes tenants need to be moved (different database, upgraded tier, etc.). I built migration tools early that can:
- Export all tenant data with referential integrity
- Import into a new environment
- Validate data consistency post-migration
Conclusion
Building EasyRHIS taught me that multi-tenant SaaS architecture requires careful planning across every layer — database, backend, frontend, and infrastructure. The key is making tenant isolation automatic and invisible to application code, while maintaining security and performance at scale.
If you're building a multi-tenant SaaS platform, start with these principles:
- Design for multi-tenancy from day one
- Enforce tenant isolation at the database level
- Never trust client-side tenant context
- Monitor and rate-limit per tenant
- Build tenant migration tools early
The result is a scalable, secure platform that can serve hundreds of restaurant locations while keeping each client's data completely isolated.
Want to discuss multi-tenant architecture for your SaaS project?
I'd be happy to share more insights from building production-scale multi-tenant systems.
Get in touch →