Skip to content

Server-Side Injection

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(sql_source="v_documents")
def documents(owner_id: str) -> list[Document]: ...
# With inject — owner_id comes from jwt.sub, clients cannot influence it
@fraiseql.query(sql_source="v_documents", inject={"owner_id": "jwt:sub"})
def my_documents() -> list[Document]: ...
@fraiseql.query(
sql_source="v_documents",
inject={"owner_id": "jwt:sub"}
)
def my_documents() -> list[Document]: ...

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(
sql_source="v_orders",
inject={"tenant_id": "jwt:tenant_id"}
)
def orders() -> list[Order]: ...

Only jwt:<claim_name> is supported. 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

Compile-time checks (fraiseql compile):

  • 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
  • Inject key must not conflict with a declared GraphQL argument on the same query or mutation

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(
sql_source="v_post",
inject={"author_id": "jwt:sub"}
)
def my_posts() -> list[Post]: ...
-- v_post filters to the injected author_id automatically
CREATE VIEW v_post AS
SELECT jsonb_build_object('id', id, 'title', title, 'author_id', author_id) AS data
FROM tb_post;

The SQL layer receives author_id = '<value from jwt.sub>' appended to the query, equivalent to WHERE author_id = $1 with a parameterized value.

Security

Security — Authentication, RBAC, and field-level authorization

Decorators

Decorators — Full decorator reference