Docs / Batteries
Database-layer security
Defense in depth from the same annotations — JWT claims propagated to Postgres GUCs and generated RLS policies, so the database enforces what the API enforces.
gqlith’s row filters and RBAC travel with the generated code. Opt in to two features and the database enforces the same model — even for a connection that bypasses your API entirely.
JWT claims → Postgres GUCs
jwt:
dbClaims:
namespace: request.jwt
transactionScoped: true
With jwt.dbClaims on, every request’s verified claims ($userId, $role, $tenantId, custom claims) are applied to the request’s transaction as Postgres settings — one batched, parameterized set_config call, scoped to the transaction so pooled connections never leak an identity. Your own SQL — RLS policies, triggers, audit functions — can read them with current_setting().
Subscriptions are covered too: each delivered event re-applies the subscriber’s claims in a short-lived transaction before the row is materialized.
Generated RLS policies
rls:
generate: true
role: app_user # non-superuser, no BYPASSRLS
gqlith generate emits generated/rls-policies.sql — idempotent PostgreSQL Row-Level Security policies compiled from the same @gqlith.rbac, @gqlith.rowFilter, @gqlith.omit and tenancy annotations that govern the API. You apply it with your own migration tool and review it in the same PR as the schema change.
The result is one authorization model, enforced twice:
- App layer — the generated API enforces RBAC, row filters and tenancy on GraphQL, REST and MCP.
- Database layer — the generated policies enforce the same predicates inside Postgres, against any client that connects.
There is no second policy language to keep in sync: annotations are the single source, and both layers are compiled from them. rls.generate requires jwt.dbClaims (the policies read the propagated claims) and fails fast at generate time if it’s missing.
Flexible claim mapping and service tokens
jwt.claimsMapmaps per-claim dotted paths, so mixed or nonstandard token shapes (Supabase, Auth0 namespaced claims, legacy issuers) slot in without a transform hook.- Service tokens are self-describing: the role (and tenant) are frozen into the signed payload, so machine-to-machine calls don’t need a users-table lookup — with opt-in live role resolution and
revokeTokensByOwnerfor immediate de-escalation.