OpenSpace Services Pvt Ltd.
OpenSpace Services Pvt Ltd.

Your experience, our tech expertise. We at OpenSpace Services are transforming businesses worldwide with our AI and cloud expertise along with custom software development. Let's discuss your project.

[email protected]
Development
  • AI/ML
  • Mobile App Development
  • Application Redesign & Modernization
  • DevOps Services
  • CMS / Website Development
  • Software & Application Development
  • End-to-End Quality Assurance
  • Strapi CMS
Data
  • Data Strategy
  • Data Governance
  • Data Engineering
  • Data Visualization
Managed Services
  • Application Managed Services
  • Website Maintenance
  • Cloud Infrastructure Managed Services
  • IT Helpdesk

  • About Us
  • Services
  • Contact Us
  • Careers
  • Blogs
  • Case Study
  • Technology Partner

© 2026 OpenSpace Services Pvt. Ltd. All rights reserved.

Privacy Policy

| India MapIndia | USA MapUSA

  1. Home
  2. Blog
  3. General
  4. Leveraging postgre sql row level security in multi tenant applications
Leveraging PostgreSQL Row Level Security in Multi-Tenant

28-mar-2025

Leveraging PostgreSQL Row Level Security in Multi-Tenant Applications

Row Level Security (RLS) in PostgreSQL is a powerful feature that enables fine-grained access control at the database level. For multi-tenant applications, especially when building scalable systems with backend architecture and database design services, where a single system serves multiple client organizations, RLS offers an elegant solution to ensure data isolation without complex application-level filtering. Let's explore how to implement and leverage this feature effectively.

Understanding the Multi-Tenant Challenge

In multi-tenant applications, one of the primary security concerns is ensuring that tenants can only access their own data. Traditionally, developers have addressed this in several ways:

  • Separate databases for each tenant (high isolation, high overhead)
  • Separate schemas for each tenant (moderate isolation, moderate overhead)
  • Shared tables with tenant_id columns and application-level filtering (low isolation, low overhead)

These challenges are commonly addressed through secure multi-tenant application development strategies.


PostgreSQL's RLS introduces a fourth approach: shared tables with database-level security policies. This approach is widely used in modern enterprise software development and data security architectures.


How PostgreSQL RLS Works

RLS allows you to define security policies that determine which rows a user can view or modify in a table. These policies are enforced at the database level, meaning they apply regardless of how the data is accessed.

Here's the basic workflow:

  • Enable RLS on a table
  • Create policies that define access rules
  • Connect to the database with appropriate role and tenant context

Implementation Example

Let's implement RLS for a multi-tenant application with the following tables:

-- Create a table to store tenant information
CREATE TABLE tenants (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    subdomain TEXT UNIQUE NOT NULL
);

-- Create a users table with tenant association
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL REFERENCES tenants(id),
    email TEXT NOT NULL,
    name TEXT NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Create a projects table with tenant association
CREATE TABLE projects (
    id SERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL REFERENCES tenants(id),
    name TEXT NOT NULL,
    description TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Create a tasks table linked to projects
CREATE TABLE tasks (
    id SERIAL PRIMARY KEY,
    project_id INTEGER NOT NULL REFERENCES projects(id),
    title TEXT NOT NULL,
    description TEXT,
    status TEXT DEFAULT 'pending',
    assigned_to INTEGER REFERENCES users(id),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

Step 1: Enable RLS on tables

Now let's enable RLS on our tables and create policies:

-- Enable RLS on the tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;

-- Create a custom function to get the current tenant ID
CREATE OR REPLACE FUNCTION current_tenant_id()
RETURNS INTEGER AS $$
BEGIN
    RETURN current_setting('app.current_tenant_id', TRUE)::INTEGER;
EXCEPTION
    WHEN OTHERS THEN
        RETURN NULL;
END;
$$ LANGUAGE plpgsql;

-- Create RLS policies for each table
CREATE POLICY tenant_isolation_policy ON users
    USING (tenant_id = current_tenant_id());

CREATE POLICY tenant_isolation_policy ON projects
    USING (tenant_id = current_tenant_id());

-- For tasks, we need to join with projects to get the tenant_id
CREATE POLICY tenant_isolation_policy ON tasks
    USING (project_id IN (SELECT id FROM projects WHERE tenant_id = current_tenant_id()));

-- By default, make tables inaccessible unless a policy explicitly allows access
ALTER TABLE users FORCE ROW LEVEL SECURITY;
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
ALTER TABLE tasks FORCE ROW LEVEL SECURITY;

Step 2: Create a mechanism to set the tenant context

When a user logs in, your application needs to establish the tenant context. Here's how you might do it in a connection-based approach:

-- For superusers/admins who need to bypass RLS
CREATE ROLE app_admin;
ALTER ROLE app_admin BYPASSRLS;

-- For regular application access
CREATE ROLE app_user;

-- When a user logs in, set the tenant context
-- This would be done in your application code
SET app.current_tenant_id = 1;  -- Replace with actual tenant ID

-- Testing the policies
SELECT * FROM users;  -- Will only return users from tenant_id = 1
SELECT * FROM projects;  -- Will only return projects from tenant_id = 1
SELECT * FROM tasks;  -- Will only return tasks associated with projects from tenant_id = 1

Step 3: Application integration

Here's an example of how you might integrate this with a Node.js application using the pg library:

const { Pool } = require('pg');
const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});

// Middleware to set tenant context
async function setTenantContext(req, res, next) {
  const client = await pool.connect();
  
  try {
    // Get tenant_id from user session or subdomain
    const tenantId = req.user.tenantId; // Or derive from subdomain/JWT
    
    // Set the tenant context for this connection
    await client.query('SET app.current_tenant_id = $1', [tenantId]);
    
    // Attach client to request for use in route handlers
    req.dbClient = client;
    
    next();
  } catch (error) {
    await client.release();
    next(error);
  }
}

// Example route using the tenant context
app.get('/projects', setTenantContext, async (req, res) => {
  try {
    const result = await req.dbClient.query('SELECT * FROM projects');
    res.json(result.rows); // Only returns projects for the current tenant
  } catch (error) {
    res.status(500).json({ error: error.message });
  } finally {
    req.dbClient.release();
  }
});

Advanced RLS Patterns

1. Dynamic Policies Based on User Roles

You can create more complex policies that take into account user roles within a tenant:

-- Create a function to get the current user's role
CREATE OR REPLACE FUNCTION current_user_role()
RETURNS TEXT AS $$
BEGIN
    RETURN current_setting('app.current_user_role', TRUE);
EXCEPTION
    WHEN OTHERS THEN
        RETURN NULL;
END;
$$ LANGUAGE plpgsql;

-- Create a policy that allows full access to admins within their tenant
CREATE POLICY admin_policy ON projects
    USING (tenant_id = current_tenant_id())
    WITH CHECK (
        tenant_id = current_tenant_id() AND
        current_user_role() = 'admin'
    );

-- Create a policy that allows read-only access to regular users
CREATE POLICY viewer_policy ON projects
    FOR SELECT
    USING (
        tenant_id = current_tenant_id() AND
        current_user_role() = 'viewer'
    );

2. Row-Level Permissions with User Ownership

For even more granular control, you can combine tenant isolation with user-specific permissions:

-- Create a function to get the current user's ID
CREATE OR REPLACE FUNCTION current_user_id()
RETURNS INTEGER AS $$
BEGIN
    RETURN current_setting('app.current_user_id', TRUE)::INTEGER;
EXCEPTION
    WHEN OTHERS THEN
        RETURN NULL;
END;
$$ LANGUAGE plpgsql;

-- Add creator_id to projects table
ALTER TABLE projects ADD COLUMN creator_id INTEGER REFERENCES users(id);

-- Policy to allow users to see all projects in their tenant,
-- but only modify ones they created
CREATE POLICY project_view_policy ON projects
    FOR SELECT
    USING (tenant_id = current_tenant_id());

CREATE POLICY project_modify_policy ON projects
    FOR UPDATE
    USING (
        tenant_id = current_tenant_id() AND
        (creator_id = current_user_id() OR current_user_role() = 'admin')
    );

Performance Considerations

While RLS provides excellent security, it does come with some performance implications:

  • Optimising these systems requires expertise in database performance optimisation and scalable system design.
  • Query Planning Overhead: RLS adds complexity to query planning.
  • Join Complications: Policies involving joins can impact performance on large tables.
  • Index Usage: Ensure proper indexing on tenant_id columns for optimal performance.

To optimize performance:

-- Add indexes on tenant_id columns
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
CREATE INDEX idx_projects_tenant_id ON projects(tenant_id);

-- For the tasks table, index the foreign key used in the join
CREATE INDEX idx_tasks_project_id ON tasks(project_id);

-- For frequently accessed data, consider partitioning by tenant_id
CREATE TABLE projects_partitioned (
    id SERIAL,
    tenant_id INTEGER NOT NULL,
    name TEXT NOT NULL,
    description TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (tenant_id, id)
) PARTITION BY LIST (tenant_id);

-- Create partitions for each tenant (or tenant group)
CREATE TABLE projects_tenant_1 PARTITION OF projects_partitioned
    FOR VALUES IN (1);

CREATE TABLE projects_tenant_2 PARTITION OF projects_partitioned
    FOR VALUES IN (2);

-- Enable RLS on the partitioned table
ALTER TABLE projects_partitioned ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON projects_partitioned
    USING (tenant_id = current_tenant_id());

Security Best Practices

  • Always test your policies: Verify that data isolation works as expected under different scenarios.
  • Use connection pooling carefully: Make sure tenant context is set correctly when reusing connections.
  • Implement application-level validation: Don't rely solely on RLS; validate tenant access at the application level too.
  • Regular security audits: Periodically review RLS policies and test for potential vulnerabilities.
  • Handle errors gracefully: Invalid tenant context should lead to empty results, not errors.

These practices align with modern application security and compliance services for enterprise systems.


Conclusion

PostgreSQL's Row Level Security provides a powerful and elegant solution for multi-tenant data isolation. By implementing security at the database level, you reduce the risk of application-level security bugs while maintaining the efficiency of a shared-table architecture.

The examples provided should give you a solid foundation for implementing RLS in your own multi-tenant applications. Remember to balance security needs with performance considerations and always test thoroughly before deploying to production.

With proper implementation, PostgreSQL RLS can significantly simplify your multi-tenant architecture while providing robust security guarantees If you're building secure multi-tenant systems, explore our custom software development and backend engineering services that your tenants can trust.

Ravindra Mistry

Written By

Ravindra Mistry

Converting Your React App to a Desktop Application with Electron

Learn how to transform your React web application into a desktop app u.......

Ravindra Mistry

2025-03-27

Next.js vs React.js: What Should You Choose?

In the world of modern web development, React.js and Next.js are two o.......

Vineeta Shah

2025-08-14

OpenSpace Office 5
About Us

Learn about OpenSpace Services, a trusted end-to-end digital product development & consulting firm, delivering software, apps, CRM & web solutions.

Author

Meet the minds behind OpenSpace—our expert authors sharing insights on software, apps, CRM, web solutions, and digital innovation.

Authors
    Category