A B2B property management company approached me after their existing multi-tenant application began leaking data between landlord accounts. Within 8 weeks, I delivered a ground-up Next.js + Supabase rebuild that achieved 100% data isolation via advanced RLS policies, 0ms Cumulative Layout Shift, and sub-400ms page loads on mobile devices — eliminating the security vulnerability that put their entire client base at risk.
Why the Legacy Architecture Collapsed
The client's original platform was a monolithic PHP application bolted onto a shared MySQL database. Tenant separation existed only at the application layer — a single ORM misconfiguration or forgotten WHERE clause would expose one landlord's financial records to another.
The symptoms were predictable:
- Data leakage incidents. Two reported cases of tenants viewing each other's lease agreements.
- Unacceptable load times. Dashboard queries scanning the entire shared table without tenant-scoped indexes returned in 3–4 seconds.
- Zero deployment confidence. Every feature push carried the risk of breaking the fragile tenant filtering logic scattered across dozens of controllers.
WordPress multi-site was briefly considered and rejected. It solves content isolation but offers nothing for structured relational data, role-based dashboards, or real-time notifications. The client needed a purpose-built system.
The Engineering Decision: Database-Level Isolation
The critical architectural choice was moving tenant isolation from the application layer to the database engine itself. Supabase's PostgreSQL Row Level Security guarantees that no query — whether from the Next.js server, a direct API call, or even a raw SQL session — can return rows belonging to a different tenant.
Every table containing tenant-scoped data carries an RLS policy:
// Simplified RLS policy creation (executed via Supabase migration)
// This runs at the Postgres level, not in application code.
// supabase/migrations/001_tenant_rls.sql
ALTER TABLE properties ENABLE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation" ON properties
USING (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid);
// Server Component: fetching properties is automatically tenant-scoped
// app/dashboard/properties/page.tsx
import { createServerClient } from '@/lib/supabase-server';
export default async function PropertiesPage() {
const supabase = await createServerClient();
// RLS automatically filters — no WHERE tenant_id clause needed
const { data: properties } = await supabase
.from('properties')
.select('id, address, unit_count, monthly_revenue')
.order('created_at', { ascending: false });
return <PropertyGrid properties={properties ?? []} />;
}
The application code never references tenant_id in queries. The database handles it. This eliminates an entire class of bugs.
Dynamic Subdomain Routing via Middleware
Each property management company accesses the platform through their own subdomain: acme.platform.com, bayshore.platform.com. Next.js middleware intercepts every request, extracts the subdomain, and resolves the tenant context before any page component renders.

The middleware adds the resolved tenant_id to the request headers. Server Components read it directly — no client-side state, no context providers, no hydration cost.
This architecture means a single Vercel deployment serves every tenant. There is no per-tenant infrastructure. Adding a new client is a single database insert.
Component Boundary Engineering
The dashboard uses a strict Server Component / Client Component split:
- Server Components handle all data fetching, tenant resolution, and layout rendering. They produce zero client-side JavaScript.
- Client Components are scoped exclusively to interactive elements: date range pickers on the revenue chart, the notification bell dropdown, and the property search filter.
This boundary discipline is the reason the platform achieves 0ms CLS. Every layout element is server-rendered and streamed. No content shifts because no client-side data fetching triggers a re-render after initial paint.
Database Optimization: Partitioned Indexes
With 200+ tenants and growing, naive full-table indexes would degrade. Every tenant-scoped table uses a composite index with tenant_id as the leading column:
// supabase/migrations/003_composite_indexes.sql
CREATE INDEX idx_properties_tenant_created
ON properties (tenant_id, created_at DESC);
CREATE INDEX idx_lease_agreements_tenant_status
ON lease_agreements (tenant_id, status, expiry_date);
// These indexes align with RLS filter patterns,
// so PostgreSQL's query planner combines the RLS predicate
// with the index scan in a single operation.
The result: dashboard queries that previously took 3,200ms on the legacy system now complete in under 80ms, even as the dataset scales.
Performance Results
| Metric | Legacy PHP App | Next.js + Supabase Build | |---|---|---| | Cumulative Layout Shift | 0.34 | 0.00 | | Mobile Page Load | 3,800ms | 380ms | | Data Isolation | Application-layer (broken) | Database-level RLS (100%) | | Deployment Model | Per-tenant VPS | Single Vercel deployment | | Time to Add New Tenant | 2–3 days (manual) | < 1 minute (DB insert) |

What the Client Received
Full repository ownership. The client holds the GitHub repository, the Supabase project credentials, and the Vercel deployment. No licensing fees, no vendor lock-in, no recurring dependency on my services.
The codebase is strictly typed end-to-end — Supabase's generated TypeScript types flow from database schema to component props without a single any cast.
Skip the technical debt. Let a senior engineer build your core architecture from scratch. View my custom Next.js engineering tiers on Fiverr.