🛡️ Security by Design
No ORM over-fetching. JSONB views define exactly what's exposed. Impossible to accidentally leak sensitive data.
Secure Your API →ORMs load entire objects by default. One innocent query can expose passwords, internal IDs, or sensitive business data.
# This loads EVERYTHING
user = User.objects.get(id=1)
# Includes: password_hash, internal_notes,
# deleted_at, admin_flags, etc.
Update operations can accidentally modify protected fields. No clear boundary between public and internal data.
# Dangerous - updates any field
User.objects.filter(id=1).update(**request.data)
# Could set: is_admin=True, balance=0, etc.
ORM lazy loading can cause unexpected data exposure through relationship traversal. Hard to audit what gets exposed.
# Seems innocent
user.posts[0].comments
# But loads: all posts, all comments,
# author details, nested relationships...
Every GraphQL type explicitly defines what fields are exposed. No implicit loading, no hidden relationships, no accidental exposure.
@fraiseql.type
class User:
# Only these fields are exposed
id: int
name: str
email: str # Explicitly chosen
# password_hash is NOT here - can't be exposed
PostgreSQL views define exactly what data is accessible. No direct table access means no accidental exposure of sensitive columns.
-- Only safe fields exposed via data JSONB
CREATE VIEW api.v_user AS
SELECT
id,
jsonb_build_object(
'id', id,
'name', name,
'email', email, -- No password_hash!
'joined_date', created_at::date
) AS data
FROM auth.users
WHERE deleted_at IS NULL;
View-enforced recursion limits prevent infinite loops and DoS attacks. No middleware needed - protection built into the database layer.
-- Limited recursion depth
CREATE VIEW v_user_posts AS
SELECT
id,
jsonb_build_object(
'id', id,
'title', title,
'depth', depth
) AS data
FROM tb_post
WHERE fk_author = current_user_id()
AND depth <= 3; -- Max 3 levels
SHA-256 + HMAC chains ensure complete audit trails. Every data access is logged with cryptographic integrity.
-- Automatic audit logging
INSERT INTO audit.log (
table_name, record_id,
operation, user_id,
old_values, new_values,
hmac_chain
) VALUES (...);
No. FraiseQL uses session variables with a shared connection pool.
All app users share one database role. FraiseQL sets SET LOCAL app.tenant_id before each query,
and RLS policies reference these via current_setting().
FraiseQL automatically sets session variables from your context before every query. PostgreSQL RLS policies then filter data based on these variables.
# Pass context when creating the repository
repo = FraiseQLRepository(db_pool, context={
"tenant_id": "abc-123", # → SET LOCAL app.tenant_id
"user_id": "user-456", # → SET LOCAL app.user_id
})
# FraiseQL automatically runs SET LOCAL before every query
# Variables are transaction-scoped and auto-cleared
-- Enable RLS on table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Tenant isolation policy (references session variable)
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id', TRUE)::UUID);
-- User + tenant policy for stricter access
CREATE POLICY user_own_orders ON orders
USING (
tenant_id = current_setting('app.tenant_id')::UUID
AND user_id = current_setting('app.user_id')::UUID
);
Many GraphQL frameworks still use string concatenation or unsafe SQL generation. Even with ORMs, complex queries can introduce injection vulnerabilities.
# Dangerous - string formatting
query = f"SELECT * FROM users WHERE name = '{user_input}'"
# Still risky with some ORMs
User.objects.raw(f"SELECT * FROM users WHERE name = '{user_input}'")
All queries use parameterized statements. PostgreSQL functions handle complex logic. No string concatenation, no injection risks.
# Safe - parameterized queries only
@fraiseql.query
async def users(info, name: str = None) -> list[User]:
return await info.context.repo.fetch(
where={"name": name} # Automatically parameterized
)
# Complex logic in PostgreSQL functions
@fraiseql.query
async def search_users(info, query: str) -> list[User]:
# Calls a safe PostgreSQL function
return await info.context.repo.call_function(
"search_users", query # Parameterized
)
Security built-in, not bolted-on. Explicit contracts prevent data leaks.