Skip to content

Server-Side Injection

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.

MechanismScopeSourceSQL access patternUse case
inject= on decoratorPer-query/mutationJWT claimsAppended as SQL parameter ($1)Row filtering, function args
[session_variables] TOMLAll queries/mutationsJWT claims or HTTP headerscurrent_setting('app.xxx')RLS policies, cross-cutting context
inject_started_atAll queries/mutationsServer clockcurrent_setting('fraiseql.started_at')Audit timestamps, consistent now()
request.jwt.claimsAll queries/mutationsFull JWT payloadcurrent_setting('request.jwt.claims')::jsonbRLS policies needing multiple claims

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

Without injection, filtering by the current user requires either:

  • Passing owner_id as a client-visible GraphQL argument (user could supply any value)
  • Writing a separate SQL view per user context

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.query
def 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 stringResolves from JWT claimTypical use
jwt:subsub (subject)Current user ID
jwt:tenant_idtenant_idMulti-tenant isolation
jwt:org_idorg_idOrganisation scoping
jwt:<any_claim>Any claim present in the tokenCustom attributes

For HTTP header injection, use [session_variables] in TOML instead.

Authoring-time checks (Python SDK, at decorator evaluation):

  • Inject key must be a valid identifier: ^[A-Za-z_][A-Za-z0-9_]*$
  • Source value must match ^jwt:[A-Za-z_][A-Za-z0-9_]*$ — other formats (e.g. header:) are rejected at the decorator level
  • Inject key must not conflict with a declared GraphQL argument on the same query or mutation

fraiseql compile checks:

  • Emits a warning when a mutation has inject params to remind about parameter ordering requirements

Runtime:

  • Unauthenticated requests to queries or mutations with inject are rejected with a validation error. There is no anonymous bypass.
  • If the specified claim is absent from the JWT, the request is rejected.

Example: Row-Level Security without PostgreSQL RLS

Section titled “Example: Row-Level Security without PostgreSQL RLS”

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

fraiseql.toml
[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 only

Per-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 variable

These are transaction-scoped — they are automatically cleared when the transaction ends. No cleanup is needed.

fraiseql.toml
[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:

FieldTypeRequiredDescription
pg_namestringyesPostgreSQL variable name (e.g., app.tenant_id)
sourcestringyes"jwt" or "header"
claimstringwhen source = "jwt"JWT claim name to extract
namestringwhen 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:

fraiseql.toml
[[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:

fraiseql.toml
[[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 view
CREATE VIEW v_product AS
SELECT id, jsonb_build_object(
'id', p.id::text,
'name', COALESCE(
t.name,
p.default_name
)
) AS data
FROM tb_product p
LEFT JOIN tb_product_translation t
ON t.fk_product = p.pk_product
AND t.locale = current_setting('app.locale', true);

When to use session variables vs. decorator inject=

Section titled “When to use session variables vs. decorator inject=”
Use caseRecommended mechanism
Filter a specific query/mutation by a JWT claimDecorator 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.

fraiseql.toml
[session_variables]
inject_started_at = true # default: true

Access in SQL:

-- Consistent request timestamp for audit trails
INSERT INTO tb_audit_log (action, occurred_at)
VALUES ('create_post', current_setting('fraiseql.started_at')::timestamptz);
-- Use in views for time-relative filtering
CREATE VIEW v_recent_activity AS
SELECT id, jsonb_build_object(...) AS data
FROM tb_activity
WHERE 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 dynamically
SELECT 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 object
CREATE 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:

  • JWT claims are extracted from the Authorization: Bearer <token> header (GraphQL, REST) or authorization metadata (gRPC)
  • HTTP headers (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 transport

The 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