Security
Security — Authentication, RBAC, and field-level authorization
FraiseQL injects server-side context into SQL queries and functions through four mechanisms. All four are enforced by the Rust runtime — no application code runs at request time.
| Mechanism | Scope | Source | SQL access pattern | Use case |
|---|---|---|---|---|
inject= on decorator | Per-query/mutation | JWT claims | Appended as SQL parameter ($1) | Row filtering, function args |
[session_variables] TOML | All queries/mutations | JWT claims or HTTP headers | current_setting('app.xxx') | RLS policies, cross-cutting context |
inject_started_at | All queries/mutations | Server clock | current_setting('fraiseql.started_at') | Audit timestamps, consistent now() |
request.jwt.claims | All queries/mutations | Full JWT payload | current_setting('request.jwt.claims')::jsonb | RLS policies needing multiple claims |
inject= ParameterThe inject parameter on @fraiseql.query and @fraiseql.mutation binds JWT claim values to SQL parameters server-side. The injected value is resolved from the verified JWT — the client never supplies it and cannot override it.
inject?Without injection, filtering by the current user requires either:
owner_id as a client-visible GraphQL argument (user could supply any value)With inject, the filter is invisible to GraphQL clients and cryptographically tied to the JWT:
# Without inject — owner_id is a client argument (insecure without extra validation)@fraiseql.querydef documents(owner_id: str) -> list[Document]: return fraiseql.config(sql_source="v_documents")
# With inject — owner_id comes from jwt.sub, clients cannot influence it@fraiseql.query(inject={"owner_id": "jwt:sub"})def my_documents() -> list[Document]: return fraiseql.config(sql_source="v_documents")@fraiseql.query(inject={"owner_id": "jwt:sub"})def my_documents() -> list[Document]: return fraiseql.config(sql_source="v_documents")The compiled SQL will include WHERE owner_id = <jwt.sub> using the value from the verified token. The owner_id column must exist in the view.
@fraiseql.mutation( sql_source="fn_create_document", inject={"created_by": "jwt:sub"})def create_document(title: str) -> Document: ...Injected values are appended after the explicit GraphQL arguments when calling the SQL function.
@fraiseql.query(inject={"tenant_id": "jwt:tenant_id"})def orders() -> list[Order]: return fraiseql.config(sql_source="v_orders")The decorator inject= parameter supports only jwt:<claim_name> sources. The claim name must be a valid identifier ([A-Za-z_][A-Za-z0-9_]*):
| Source string | Resolves from JWT claim | Typical use |
|---|---|---|
jwt:sub | sub (subject) | Current user ID |
jwt:tenant_id | tenant_id | Multi-tenant isolation |
jwt:org_id | org_id | Organisation scoping |
jwt:<any_claim> | Any claim present in the token | Custom attributes |
For HTTP header injection, use [session_variables] in TOML instead.
Authoring-time checks (Python SDK, at decorator evaluation):
^[A-Za-z_][A-Za-z0-9_]*$^jwt:[A-Za-z_][A-Za-z0-9_]*$ — other formats (e.g. header:) are rejected at the decorator levelfraiseql compile checks:
Runtime:
inject are rejected with a validation error. There is no anonymous bypass.inject is an alternative to PostgreSQL ROW LEVEL SECURITY policies for simpler setups:
@fraiseql.query(inject={"author_id": "jwt:sub"})def my_posts() -> list[Post]: return fraiseql.config(sql_source="v_post")-- FraiseQL runtime appends WHERE author_id = $1 using the jwt:sub valueCREATE VIEW v_post AS SELECT p.id, jsonb_build_object('id', p.id::text, 'title', p.title, 'author_id', p.author_id) AS data FROM tb_post p;The SQL layer receives author_id = '<value from jwt.sub>' appended to the query, equivalent to WHERE author_id = $1 with a parameterized value.
The [inject_defaults] TOML section applies inject= parameters globally to all queries and/or mutations, so you don’t need to repeat inject={"tenant_id": "jwt:tenant_id"} on every decorator.
[inject_defaults]tenant_id = "jwt:tenant_id" # applied to ALL queries and mutations
[inject_defaults.queries]# Query-specific overrides (merged with top-level defaults)
[inject_defaults.mutations]user_id = "jwt:sub" # applied to mutations onlyPer-decorator inject= overrides take precedence over defaults. The tenant_scoped=True option on @fraiseql.type generates this configuration automatically — see Multi-Tenancy for details.
The [session_variables] TOML section injects per-request context into PostgreSQL via SET LOCAL before each query or mutation. Unlike decorator inject= (which passes values as SQL function parameters), session variables are set as PostgreSQL GUC variables that any SQL — views, functions, RLS policies, triggers — can read with current_setting().
On every request, before the SQL executes, the Rust runtime runs:
SET LOCAL app.tenant_id = '<value from JWT>';SET LOCAL app.locale = '<value from Accept-Language header>';-- ... one SET LOCAL per configured variableThese are transaction-scoped — they are automatically cleared when the transaction ends. No cleanup is needed.
[session_variables]inject_started_at = true # see "Automatic Timestamp Injection" below
[[session_variables.variables]]pg_name = "app.tenant_id"source = "jwt"claim = "tenant_id"
[[session_variables.variables]]pg_name = "app.user_id"source = "jwt"claim = "sub"
[[session_variables.variables]]pg_name = "app.locale"source = "header"name = "Accept-Language"Each [[session_variables.variables]] entry maps a PostgreSQL variable to a request context source:
| Field | Type | Required | Description |
|---|---|---|---|
pg_name | string | yes | PostgreSQL variable name (e.g., app.tenant_id) |
source | string | yes | "jwt" or "header" |
claim | string | when source = "jwt" | JWT claim name to extract |
name | string | when source = "header" | HTTP header name to extract |
Use app.* namespaced variables to avoid conflicts with built-in PostgreSQL settings.
Extract a verified JWT claim and set it as a session variable:
[[session_variables.variables]]pg_name = "app.tenant_id"source = "jwt"claim = "tenant_id"Your RLS policies can then reference it directly:
CREATE POLICY tenant_isolation ON tb_post USING (tenant_id = current_setting('app.tenant_id')::uuid);Extract an HTTP request header value:
[[session_variables.variables]]pg_name = "app.locale"source = "header"name = "Accept-Language"
[[session_variables.variables]]pg_name = "app.correlation_id"source = "header"name = "X-Correlation-ID"-- Use the client's locale in a viewCREATE VIEW v_product ASSELECT id, jsonb_build_object( 'id', p.id::text, 'name', COALESCE( t.name, p.default_name )) AS dataFROM tb_product pLEFT JOIN tb_product_translation t ON t.fk_product = p.pk_product AND t.locale = current_setting('app.locale', true);inject=| Use case | Recommended mechanism |
|---|---|
| Filter a specific query/mutation by a JWT claim | Decorator inject= — explicit, per-operation |
| RLS policies that need tenant context on every table | [session_variables] — set once, available to all SQL |
| Pass HTTP headers (locale, correlation ID) to SQL | [session_variables] with source = "header" |
| Audit triggers that need the current user | [session_variables] with source = "jwt" |
Both mechanisms can be used together. For example, use [session_variables] to set app.tenant_id for RLS, and use decorator inject= to pass tenant_id as a function parameter to a specific mutation that needs it explicitly.
By default, the Rust runtime sets a fraiseql.started_at session variable with the timestamp of when request processing began. This provides a consistent “request time” that doesn’t drift across multiple SQL statements within the same transaction.
[session_variables]inject_started_at = true # default: trueAccess in SQL:
-- Consistent request timestamp for audit trailsINSERT INTO tb_audit_log (action, occurred_at)VALUES ('create_post', current_setting('fraiseql.started_at')::timestamptz);
-- Use in views for time-relative filteringCREATE VIEW v_recent_activity ASSELECT id, jsonb_build_object(...) AS dataFROM tb_activityWHERE created_at > current_setting('fraiseql.started_at')::timestamptz - interval '24 hours';Unlike now() or clock_timestamp(), fraiseql.started_at is the same value throughout the entire request — even if the transaction contains multiple statements. This is useful when you want all audit rows from a single request to share the same timestamp.
Set inject_started_at = false to disable this if you don’t need it.
In addition to individual claim extraction, FraiseQL sets the entire verified JWT payload as a single session variable: request.jwt.claims. This is a JSONB-serialized string containing all claims from the token.
-- Extract any claim dynamicallySELECT current_setting('request.jwt.claims', true)::jsonb ->> 'sub' AS user_id;SELECT current_setting('request.jwt.claims', true)::jsonb ->> 'email' AS email;This is useful for RLS policies that need access to multiple claims without declaring each one as a separate [session_variables] entry:
-- RLS policy using the full claims objectCREATE POLICY project_isolation ON tb_project USING ( fk_organization IN ( SELECT fk_organization FROM tb_membership WHERE fk_user = ( SELECT pk_user FROM tb_user WHERE id = (current_setting('request.jwt.claims', true)::jsonb ->> 'sub')::uuid ) ) );The second argument true in current_setting('request.jwt.claims', true) returns NULL instead of raising an error on unauthenticated requests. Always use this form in RLS policies to avoid blocking public queries on tables with mixed-access policies.
See the Multi-Tenant SaaS example for a complete application using this pattern.
All injection mechanisms work identically across GraphQL, REST, and gRPC transports:
Authorization: Bearer <token> header (GraphQL, REST) or authorization metadata (gRPC)source = "header") are read from the transport layer directly — this includes REST and GraphQL HTTP requests. For gRPC, headers are read from metadata.fraiseql.started_at and request.jwt.claims are set regardless of transportThe same RLS policies, session variables, and inject parameters work whether the client uses GraphQL, REST, or gRPC.
Security
Security — Authentication, RBAC, and field-level authorization
Multi-Tenancy
Multi-Tenancy — Tenant isolation patterns with RLS and inject
Decorators
Decorators — Full decorator reference including inject=
TOML Config
TOML Config — [session_variables] and [inject_defaults] reference
Authentication
Authentication — JWT verification and claim injection examples