Multi-Tenancy Guide
Row-Level Security patterns, schema-per-tenant, and database-per-tenant strategies. Multi-Tenancy
SaaS companies need tenant isolation, OAuth flows, rate limiting, and audit logging — typically wired up separately across multiple libraries. FraiseQL provides all of this through configuration, backed by PostgreSQL’s row-level security enforcement.
| Concern | How FraiseQL Handles It |
|---|---|
| Tenant isolation | PostgreSQL RLS policies + JWT inject= pattern |
| Authentication | JWT verification in the Rust runtime on every request |
| OAuth / PKCE flows | [security.pkce] — browser-safe PKCE code flow |
| Rate limiting | [security.rate_limiting] — per-authenticated-user token bucket |
| Audit logging | [security.enterprise] with audit_log_backend = "postgresql" |
| Error sanitization | [security.error_sanitization] — hides internals from clients in production |
FraiseQL injects the verified tenant_id JWT claim into every SQL call. Your PostgreSQL RLS policies enforce that each tenant only sees their own rows.
-- Tenant-isolated tableCREATE TABLE tb_project ( pk_project BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id UUID DEFAULT gen_random_uuid() NOT NULL UNIQUE, tenant_id UUID NOT NULL, name TEXT NOT NULL);
ALTER TABLE tb_project ENABLE ROW LEVEL SECURITY;
CREATE POLICY project_tenant_isolation ON tb_project USING (tenant_id = current_setting('app.tenant_id')::UUID);import fraiseqlfrom fraiseql.scalars import ID
@fraiseql.typeclass Project: id: ID name: str
@fraiseql.query( inject={"tenant_id": "jwt:tenant_id"})def projects(limit: int = 20) -> list[Project]: return fraiseql.config(sql_source="v_project")tenant_id is never in the GraphQL schema. The Rust runtime injects it from the verified JWT. The RLS policy does the rest — even if application code is compromised, the database enforces isolation.
[security]default_policy = "authenticated"
[security.pkce]enabled = truecode_challenge_method = "S256"state_ttl_secs = 600For browser-facing SaaS applications, configure PKCE to handle the OAuth code flow:
[security.pkce]enabled = truecode_challenge_method = "S256"state_ttl_secs = 600issuer_url = "https://accounts.google.com"client_id = "my-fraiseql-client"redirect_uri = "https://api.example.com/auth/callback"The OIDC client secret is set via OIDC_CLIENT_SECRET — never in fraiseql.toml.
[security.rate_limiting]enabled = truerequests_per_second = 100requests_per_second_per_user = 500 # authenticated tenants get 5× the global rateburst_size = 200
# Brute-force protection on auth endpointsfailed_login_max_requests = 5failed_login_window_secs = 3600Authenticated users receive a higher per-user limit than the global rate. The global rate acts as a floor for unauthenticated traffic. Rate limits apply identically across GraphQL and REST transports.
[security.enterprise]audit_logging_enabled = trueaudit_log_backend = "postgresql"Create the audit table. This example adds tenant_id for multi-tenant isolation — see Security reference for the full canonical schema including variables, user_agent, and error_message columns.
CREATE TABLE ta_audit_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), timestamp TIMESTAMPTZ DEFAULT NOW(), user_id UUID, tenant_id UUID, operation TEXT NOT NULL, query_name TEXT, ip_address INET, duration_ms INTEGER, success BOOLEAN);FraiseQL writes one row per request to ta_audit_log. You own the table — query it directly for compliance reports or forward it to your SIEM.
[security.error_sanitization]enabled = truehide_implementation_details = truesanitize_database_errors = truecustom_error_message = "An internal error occurred"Without this, raw PostgreSQL error messages reach clients. Enable it in every production deployment.
The fraiseql-starter-saas template includes the full multi-tenant data model — tenant table, user table, subscription table, PostgreSQL RLS policies on every table, JWT tenant_id claim enforcement, role-based access (admin/member), and an invite flow mutation.
git clone https://github.com/fraiseql/fraiseql-starter-saascd fraiseql-starter-saascp .env.example .envdocker compose upClone the SaaS starter
git clone https://github.com/fraiseql/fraiseql-starter-saasSet up your environment
cp .env.example .env# Edit .env: set DATABASE_URL, JWT_SECRET, OIDC_CLIENT_SECRETStart services
docker compose upExtend the schema — add your domain entities following the existing RLS pattern
Enable production features — add [security.error_sanitization], [security.enterprise], and [security.rate_limiting] to fraiseql.toml
Multi-Tenancy Guide
Row-Level Security patterns, schema-per-tenant, and database-per-tenant strategies. Multi-Tenancy
Authentication
JWT verification, PKCE flows, API keys, and token revocation. Authentication
Security Reference
Full reference for RBAC, error sanitization, rate limiting, and audit logging. Security
SaaS Starter
Working multi-tenant project with RLS, JWT, and role-based access. Starter Templates