Akshar KaPatel logo
Product Architecture20248-10 min read

SaaS Architecture Patterns for Scalable Platforms

Akshar KaPatel

System Architect & Founder of Brotech IT Services

Core Insight:Scalability is not a feature you add later. It is an architectural decision made at the foundation.

Building multi-tenant Software-as-a-Service (SaaS) platforms requires finding the sweet spot between hosting costs, system performance, and tenant isolation compliance. A single database leakage incident can destroy a startup's reputation. Developers must design databases and file stores so that data scoping is automated and secure.

This research paper explores SaaS tenancy architectures, comparing database-per-tenant, schema-per-tenant, and shared-schema designs. It provides concrete examples for implementing PostgreSQL Row-Level Security (RLS), dynamic AWS S3 directory isolation middleware, and composite indexing.

1. Tenancy Models Comparison

When architecting SaaS platforms, we evaluate three database separation models:

  1. Database-Per-Tenant: Every tenant receives a dedicated database. Highest isolation security, but raises host overhead costs and complicates database updates (e.g. running migrations across 1,000 databases).
  2. Schema-Per-Tenant: Tenants share the same database cluster but reside inside distinct database schemas. High speed, but migration scripts remain complex.
  3. Shared Database, Shared Schema: Tenants share tables. Columns include a tenant_id. Highly cost-effective and simple to manage, but requires automated query scoping to prevent leakages.

2. PostgreSQL Row-Level Security (RLS)

Instead of managing tenant filtering solely in the application code, we can configure Row-Level Security (RLS) directly in the database engine. In PostgreSQL, we enable RLS policies on tables, ensuring queries automatically filter rows using active session parameters:

PostgreSQL RLS Configuration Script

-- 1. Enable RLS on the target table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- 2. Create the policy checking current session variable
CREATE POLICY tenant_isolation_policy ON orders
    FOR ALL
    USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::integer);

-- 3. Application database connection helper:
-- Before resolving any order query, the web server executes:
-- SET LOCAL app.current_tenant_id = 14;
-- Subsequent queries like 'SELECT * FROM orders' are automatically scoped.

3. Isolating Tenant File Storage (AWS S3)

Separating databases is only half the battle. Document storage (like invoices, receipts, and client profile attachments) must also be isolated. We implement Laravel middleware that dynamically scopes the S3 storage root directory for each tenant request:

Laravel Tenant File Isolation Middleware

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Auth;

class ScopeTenantS3Storage
{
    public function handle(Request $request, Closure $next)
    {
        if (Auth::check()) {
            $tenantId = Auth::user()->tenant_id;
            
            // Set dynamic S3 prefix for current request thread
            config([
                'filesystems.disks.s3.root' => "tenants/tenant_{$tenantId}/uploads",
            ]);
            
            // Re-resolve the filesystem drive to apply config overrides
            Storage::forgetDisk('s3');
        }

        return $next($request);
    }
}

4. Architecture Diagram

This visual outlines request routing: incoming HTTP headers identify the tenant, which configures database session variables before querying shared clusters.

Tenant ATenant BAuth Gateway(Scopes Tenant)Shared DB EngineWHERE tenant_id = X

5. Performance Benchmarks

The table outlines database query performance across different tenant models at high scale (10,000,000 orders database benchmark):

Infrastructure StyleQuery LatencyMigration ComplexityCompute Cost
Database-Per-Tenant0.31 msHigh (Separate migrations)High ($$$ / month)
Shared DB, Shared Schema (No Index)1,240 msVery LowMedium (CPU spikes)
Shared DB + Tenant Scoping Index0.42 msVery LowLow ($ / month)
PostgreSQL RLS Enabled0.44 msLowLow ($ / month)

Database-Per-Tenant: Isolating schemas provides the fastest query latency (0.31 ms) because index trees remain tiny. However, this approach dramatically increases management complexity during schema migration rollouts and raises hardware hosting costs.

Shared DB, Shared Schema (No Index): Failing to index the scoping column results in table scans that trigger high query latencies (1,240 ms) and server CPU compute spikes. This demonstrates why query optimization is crucial in shared-schema setups.

Shared DB + Tenant Scoping Index: Implementing composite indexing on tenant and primary keys reduces search latencies to 0.42 ms. This maintains excellent query speed and low compute costs while using a single, easily manageable schema.

PostgreSQL RLS Enabled: Leveraging database-level Row-Level Security policies delivers strict data isolation with minimal query overhead (0.44 ms). This configuration ensures robust security compliance without incurring the cost of separate servers.

Conclusion

Building scalable SaaS platforms requires embedding tenant filters deep inside the system. By combining database Row-Level Security with dynamic AWS storage middleware and composite index designs, we achieve high-speed queries and strict tenant isolation at a lower hosting price point.

Related Architectural Studies