Build Multi-Tenant SaaS the AI-Era Way
LLMs can generate your entire backend because the patterns are consistent and token-efficient.
The Challenge: Multi-Tenant Complexity
Enterprise SaaS requires:
- Row-level isolation - One database, multiple tenants, zero data bleeding
- RBAC enforcement - Users, roles, permissions scattered across resolvers
- Consistent patterns - Every API endpoint needs the same isolation logic
- AI-generated code - LLMs struggle to learn your custom resolver patterns
- Performance tuning - Each new relationship needs optimization
Traditional GraphQL answers: write field resolvers for every relationship. This leads to 47 different custom patterns, none of which an LLM can predict.
FraiseQL Solution: Database-First Isolation
You write SQL views. The database handles isolation. Your code becomes simple and repetitive—exactly what LLMs love.
1. Define Tenant Context in Views
PostgreSQL views with tenant filtering:
CREATE VIEW v_user_posts AS
SELECT
jsonb_build_object(
'id', p.id::text,
'title', p.title,
'author_id', p.author_id::text,
'created_at', p.created_at
) AS data
FROM tb_post p
WHERE p.tenant_id = current_setting('app.tenant_id')::uuid; 2. Set Tenant Context Once
Declare the query. The Rust runtime sets tenant context automatically:
@fraiseql.query(sql_source="v_user_posts")
def user_posts(user_id: ID) -> list[UserPost]:
# Tenant context is set by the Rust runtime via fraiseql.toml
# All views enforce it automatically via current_setting('app.tenant_id')
pass 3. Define Python Types
Simple, repeatable schema:
@fraiseql.type
class UserPost:
id: ID
title: str
author_id: ID
created_at: str Result: Every resolver follows the same pattern. 4 core patterns instead of 47 custom ones. LLMs can generate your entire API.
Real-World Example: Team Workspace SaaS
Platform: Organizations have Teams, Teams have Projects, Projects have Tasks
Challenge: Fetch team's projects with task counts
Traditional GraphQL problem: N+1 queries across 3 tables + authorization checks in each resolver
FraiseQL approach:
-- Single SQL view handles everything
CREATE VIEW v_team_projects AS
SELECT
jsonb_build_object(
'id', t.id::text,
'name', t.name,
'projects', (
SELECT jsonb_agg(jsonb_build_object(
'id', p.id::text,
'name', p.name,
'task_count', (SELECT count(*) FROM tb_task WHERE project_id = p.id)
))
FROM tb_project p
WHERE p.team_id = t.id
AND p.tenant_id = current_setting('app.tenant_id')::uuid
)
) AS data
FROM tb_team t
WHERE t.tenant_id = current_setting('app.tenant_id')::uuid; One query. One resolver. Done.
@fraiseql.query(sql_source="v_team_projects")
def team_with_projects(team_id: ID) -> TeamProjects:
pass Performance Impact
Advanced Patterns for Enterprise
Multi-Database Tenants
If you use database-per-tenant instead of schema-per-tenant:
@fraiseql.query(sql_source="v_user")
def user_by_tenant(user_id: ID) -> User:
# Per-tenant database routing is configured in fraiseql.toml
# The Rust runtime selects the correct connection pool
pass Role-Based Access Control
Enforce in views with user role context:
CREATE VIEW v_user_reports AS
SELECT
jsonb_build_object(
'id', r.id::text,
'title', r.title,
'data', CASE
WHEN current_setting('app.user_role') = 'admin' THEN r.data
WHEN current_setting('app.user_role') = 'editor' THEN jsonb_delete(r.data, '{sensitive_metrics}')
ELSE NULL
END
) AS data
FROM tb_report r
WHERE r.tenant_id = current_setting('app.tenant_id')::uuid; Audit Logging
Automatic audit trail of what data was accessed:
CREATE TRIGGER audit_user_access AFTER SELECT ON v_user
FOR EACH ROW EXECUTE FUNCTION log_data_access(
'user_view',
NEW.id,
current_setting('app.tenant_id'),
current_setting('app.user_id')
); Related Resources
Ready to Scale Your SaaS?
Get started with FraiseQL and build your next multi-tenant platform faster than ever.