Case Study: Secure Multi-Tenant Real Estate Property Management Platform

By Oleh FrozPublished on June 3, 2026

Executive Summary

This case study analyzes the architecture and implementation of a custom, multi-tenant real estate property management dashboard. The client required a platform capable of handling two distinct user classes—landlords and tenants—with absolute data isolation and real-time communication capabilities. The solution leverages Next.js Server Components for high-performance rendering, Supabase for authentication and database management, PostgreSQL Row-Level Security (RLS) for data protection, and TypeScript for end-to-end type safety. The finished platform achieves zero cross-tenant data exposure and real-time messaging latency below 100 milliseconds.


The Challenge: Multi-Tenant Isolation and Real-Time Sync

A property management portal is inherently multi-tenant. Landlords manage distinct portfolios of properties, leases, financial transactions, and maintenance requests. Tenants must only access data associated with their specific lease agreements.

Landlord and Tenant Role Segregation

The core security risk was cross-tenant data exposure. The application required a strict role-based access control (RBAC) model. If a landlord requests a list of leases, the database must filter the query to return only their assets. Conversely, a tenant querying their monthly invoice must never receive access to another tenant's financial ledger, even if they manipulate query parameters. Checking these permissions solely in the frontend or at the API routing layer introduces security vulnerabilities.

Low-Latency Communication Requirements

Landlords and tenants require a secure channel to discuss lease terms and maintenance issues. Traditional polling mechanisms generate unnecessary database load and result in poor user experience. The application needed a real-time messaging system capable of distributing messages to active users instantly, maintaining read/unread states, and persisting logs for dispute resolution.


The Solution: Row-Level Security and Event-Driven Actions

We rejected the model of running a separate database instance per landlord due to maintenance complexity and hosting costs. Instead, we implemented a shared-database, logical-isolation architecture.

Implementing RLS for Role-Based Isolation

Data protection is enforced at the database layer using PostgreSQL Row-Level Security (RLS). Every database query executed by the client implicitly passes the authenticated user's session token (auth.uid()). We designed tables for properties, leases, profiles, and messages with specific security policies.

For example, the policy on the leases table checks the requesting user's ID against the landlord ID or tenant ID associated with the lease. If neither ID matches the current session, PostgreSQL blocks read and write operations.

Server Actions and Type-Safe State Management

To modify data, the application uses Next.js Server Actions. These run exclusively on the server and are protected by authentication checks. Input validation is handled using the zod library. By typing all database responses with TypeScript interfaces, we guarantee that the frontend code matches the database schema. This eliminates run-time errors caused by schema drift.

Real-Time Chat Synchronization

The chat feature leverages Supabase Realtime WebSockets. When a message is written to the messages table via a Server Action, PostgreSQL updates its write-ahead log. The Supabase Realtime container detects this write and broadcasts the payload to active WebSocket connections listening to that channel. The client-side component updates the message UI instantly without executing full page refreshes.


Technical Implementation and Code Architecture

The following database schema defines the relationship between users, properties, and leases, including the RLS policies that enforce tenant isolation.

// Database Schema and Row-Level Security Policy Definitions
// Place inside your migration folder (e.g., supabase/migrations/)

/* 
CREATE TABLE profiles (
  id uuid REFERENCES auth.users ON DELETE CASCADE PRIMARY KEY,
  role text CHECK (role IN ('landlord', 'tenant')) NOT NULL,
  full_name text NOT NULL
);

CREATE TABLE properties (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  landlord_id uuid REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
  address text NOT NULL,
  created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);

CREATE TABLE leases (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  property_id uuid REFERENCES properties(id) ON DELETE CASCADE NOT NULL,
  tenant_id uuid REFERENCES profiles(id) ON DELETE SET NULL,
  monthly_rent numeric NOT NULL,
  start_date date NOT NULL,
  end_date date NOT NULL
);

-- Enable <a href="/glossary/row-level-security" title="What is Row Level Security?" class="text-blue-600 dark:text-blue-400 underline decoration-blue-300 dark:decoration-blue-700 underline-offset-2 hover:decoration-blue-500 transition-colors">Row Level Security</a> on all tables
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE properties ENABLE ROW LEVEL SECURITY;
ALTER TABLE leases ENABLE ROW LEVEL SECURITY;

-- Security Policies for properties table
CREATE POLICY "Landlords can manage their own properties"
  ON properties
  FOR ALL
  USING (auth.uid() = landlord_id);

CREATE POLICY "Tenants can view properties they occupy"
  ON properties
  FOR SELECT
  USING (
    EXISTS (
      SELECT 1 FROM leases 
      WHERE leases.property_id = properties.id 
      AND leases.tenant_id = auth.uid()
    )
  );

-- Security Policies for leases table
CREATE POLICY "Landlords can view and manage leases for their properties"
  ON leases
  FOR ALL
  USING (
    EXISTS (
      SELECT 1 FROM properties 
      WHERE properties.id = leases.property_id 
      AND properties.landlord_id = auth.uid()
    )
  );

CREATE POLICY "Tenants can view their own leases"
  ON leases
  FOR SELECT
  USING (tenant_id = auth.uid());
*/

import { createClient } from '@/lib/supabase/client';

export interface Lease {
  id: string;
  property_id: string;
  tenant_id: string | null;
  monthly_rent: number;
  start_date: string;
  end_date: string;
}

export async function fetchUserLeases(): Promise<Lease[]> {
  const supabase = createClient();
  
  // The authenticated session is automatically attached to the client header.
  // PostgreSQL evaluates RLS policies before executing the query.
  const { data, error } = await supabase
    .from('leases')
    .select('*');

  if (error) {
    throw new Error(`Failed to fetch leases: ${error.message}`);
  }

  return (data || []) as Lease[];
}

Business Outcomes and Performance Metrics

By enforcing security at the database layer and loading assets statically via Server Components, the platform achieved the following metrics:

  • Zero Data Leakage: Audits confirmed that tenant data remained isolated during unauthorized URL parameter modification attempts.
  • Fast Load Times: The properties listing dashboard rendered in less than 400 milliseconds, passing all Core Web Vitals checks.
  • Development Efficiency: Using Next.js Server Actions and Supabase eliminated the need to write and deploy a separate API server, reducing development time by 35%.
    Case Study: Secure Multi-Tenant Real Estate Property Management Platform | Froz | Froz Web Engineering