🛡️ Security by Design

Explicit Field Contracts

No ORM over-fetching. JSONB views define exactly what's exposed. Impossible to accidentally leak sensitive data.

Secure Your API →

Security Comparison

❌ ORM Security Issues

• Over-fetching by default
• Accidental field exposure
• Mass assignment vulnerabilities
• Hidden relationships
• Complex permission logic
Data leaks waiting to happen

✅ FraiseQL Security

• Explicit field contracts
• JSONB view whitelisting
• No over-fetching possible
• Clear data boundaries
• Built-in audit trails
Security guaranteed by design

The ORM Security Nightmare

Over-Fetching

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.

Mass Assignment

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.

Hidden Relationships

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...

FraiseQL's Security Architecture

🔒 Explicit Field Contracts

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

🛡️ JSONB View Whitelisting

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;

🔍 Recursion Depth Protection

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

📊 Cryptographic Audit Logging

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 (...);

Row-Level Security Integration

🤔 Common Question: Do I need one database user per app user?

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().

How FraiseQL + RLS Works

FraiseQL automatically sets session variables from your context before every query. PostgreSQL RLS policies then filter data based on these variables.

Python - FraiseQL Context
# 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
SQL - RLS Policy
-- 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
    );

Why This Is Secure

  • SET LOCAL is transaction-scoped: Variables auto-clear when transaction ends
  • Database-level enforcement: Even bugs in app code can't bypass RLS
  • No connection leakage: Each request gets fresh session state
  • Shared pool efficiency: No need to manage thousands of DB users

SQL Injection Protection

The Traditional Problem

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}'")

FraiseQL's Approach

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
    )

Ready for Secure GraphQL?

Security built-in, not bolted-on. Explicit contracts prevent data leaks.