Generated code contains no authorization checks: the database is the enforcement point (see the Security Model and Row-Level Security and the integrity gradient below). Everything else about how a request reaches the database, connection acquisition, transaction demarcation, and mounting the caller’s identity, comes in one of two shapes, and this document is the reference for both:
-
The owned-connection path (R429, recommended). Graphitron owns the connection lifecycle. You give it a
DataSourceand a dialect once; per request it pins one connection, mounts the caller’s identity on it through database session hooks you configure, commits each mutation field independently, and unmounts and releases at completion. See The owned-connection path. -
The escape-hatch path. You bring your own
DSLContextper request and own transaction demarcation and identity yourself; Graphitron’s owned-connection guarantees do not apply. This is the lower-opinion path built onGraphitronContext. See The escape-hatch path.
Both paths read per-request values the same way, through the emitted
GraphitronContext and the Graphitron.newExecutionInput(…) /
Graphitron.newOwnedExecutionInput(…) factories. For the minimal wiring that
stands up a working endpoint, see the
tutorial; this file picks up where that leaves off.
The owned-connection path
Build the runtime once at wiring time over your pooled DataSource and jOOQ
dialect; you (or your framework) still own pool creation and tuning, Graphitron
owns acquisition, transactions, and identity on top of it:
var runtime = Graphitron.runtime(dataSource, SQLDialect.POSTGRES);
var engine = runtime.newGraphQL(Graphitron.buildSchema(b -> {})).build();
runtime.newGraphQL(schema) attaches the connection-lifecycle instrumentation,
so you register nothing else. Per request you pass only the opaque claims
payload (and any declared contextArguments):
var input = Graphitron.newOwnedExecutionInput(claims, userId).query(q).build();
For that operation Graphitron pins one connection, runs your connect hook to mount identity, executes (each top-level mutation field in its own transaction that commits or rolls back independently, queries in autocommit), then runs your disconnect hook and releases the connection. A connection whose unmount failed is evicted, never returned to the pool.
Session identity through <sessionState>
Identity goes to the database, not to Java. The claims payload you pass is
handed untouched to a connect callable you name in the plugin’s
<sessionState> configuration; a paired disconnect callable clears it.
Graphitron parses nothing about the payload, the database does. See
Session
identity for the two configuration forms (the function-hook form for
Oracle/RAS or any privilege-fenced package, and the Postgres <variables>
sugar) and the pairing rules.
The hook state contract: session-scoped, never transactional
Session identity is connection-scoped state, and your hooks must treat it that
way. Graphitron cannot see inside a connect or disconnect routine you author, so
the contract is stated here and holds for every function-hook <sessionState>:
-
Set and reset session-scoped state only. On Postgres that means
set_config(key, value, false)(the third argumentfalsemeans session-scoped), neverSET LOCALand neverset_config(…, true), both of which are transaction-scoped and vanish at the next commit or rollback. Oracle RAS attach/detach and VPD context calls are session operations and already comply. -
Do not rely on a surrounding transaction. Neither mount nor unmount may depend on any transaction committing or rolling back: no state that a commit wipes (for example a global temporary table declared
ON COMMIT DELETE ROWS), no work left uncommitted for a later commit to persist. -
You may assume no transaction is open. Graphitron invokes both hooks outside any open transaction, structurally: acquisition normalizes the pinned connection to autocommit before connect runs (so a pool configured autocommit=false cannot put your mount inside an implicit never-committed transaction), and release settles any transaction the operation left open before disconnect runs (so your clears take effect immediately rather than being reverted by the pool’s return-rollback).
A hook that violates the first two points can leave identity mounted for the pool’s next borrower or drop it mid-operation; both are security defects, not performance defects.
Because graphitron cannot verify the second point from outside, it does not
trust documentation alone. Unless the configuration declares
<stateSurvivesTransactions>true</stateSurvivesTransactions>, a paired
function hook is re-fired after each top-level mutation-field settle
(disconnect with the old handle, connect capturing a fresh one, in autocommit),
so a settle can never leave stale or reverted identity for the field’s
read-back or later mutation fields. Queries never settle, so the safe default
never taxes the read path. The <variables> sugar needs no declaration: its
mounts commit immediately in autocommit and session-scoped variables survive
settles, so it opts in structurally. One scoped exception: the dev-only
ROLLBACK_ONLY commit policy (the R428 execute tool) defers a single operation
transaction across field boundaries, so nothing settles mid-operation and the
re-fire never runs there; dev execution does not exercise an unconfirmed
re-fire pair between mutation fields. See
Session
identity for the configuration.
If you configure no <sessionState>, Graphitron mounts no identity and warns at
build time (no-session-state): an unsecured direct-to-database API is a
data-exposure risk. Silence that rule only if the API is intentionally
unsecured or uses the escape hatch exclusively.
Per-tenant routing
For database-per-tenant deployments, build the runtime over a default
DataSource plus a Map<TenantId, DataSource>; Graphitron pins one connection
per distinct divined tenant within an operation and mounts identity on each. The
schema-side inference that divines the tenant key is R45’s tenant-column work;
the runtime seam it rides is part of this path.
The escape-hatch path: GraphitronContext
On the escape-hatch path you bring your own DSLContext per request through
Graphitron.newExecutionInput(dsl, …) and own transaction demarcation and
identity yourself; Graphitron’s owned-connection guarantees do not apply. The
escape-hatch engine (Graphitron.newGraphQL()) attaches no connection-lifecycle
instrumentation and logs a one-time notice to that effect at wiring time. This
is the right path when you already own a request-scoped DSLContext (an
existing transaction manager, a framework-provided connection) and want
Graphitron to use it rather than acquire its own.
GraphitronContext is the sealed per-request contract every generated DataFetcher reads from. It is emitted per app under <outputPackage>.schema and sealed to permit only the generator’s own GraphitronContextImpl singleton (nested inside the interface to inherit same-compilation-unit permits); apps no longer implement it directly. The extension surface moves to the points where per-request values cross the boundary:
-
Per-request
DSLContext: pass it as the first parameter ofGraphitron.newExecutionInput(dsl, …). On the owned-connection path you do not supply one, the instrumentation produces it from the pinned connection. -
Per-request
contextArgumentvalues: pass each one as a typed parameter toGraphitron.newExecutionInput(…). The factory’s parameter list reflects the schema’s declaredcontextArgumentsand their reflected Java types, so the consumer’s request-entry code threads each value through a typed slot rather than stashing arbitrary entries onGraphQLContextby hand. -
Custom validator factory: covered by a follow-up Mojo configuration item (R192). The default reads
Validation.buildDefaultValidatorFactory().getValidator(); this is unchanged by R190 and the single existing override point is currently unused, so no migration is required.
// GENERATED (illustrative shape; full method set evolves with the schema)
public sealed interface GraphitronContext {
default DSLContext getDslContext(DataFetchingEnvironment env) { ... }
default Object getContextArgument(DataFetchingEnvironment env, String name) { ... }
default Validator getValidator(DataFetchingEnvironment env) { ... }
final class GraphitronContextImpl implements GraphitronContext {
public static final GraphitronContextImpl INSTANCE = new GraphitronContextImpl();
private GraphitronContextImpl() {}
}
}
getContextArgument reads the value from env.getGraphQlContext() and returns it as Object; the cast to the expected Java type happens at the generated call site ((String) graphitronContext(env).getContextArgument(env, "userId")). A missing entry throws IllegalStateException naming the contextArgument and pointing at Graphitron.newExecutionInput(…); a wrong-typed entry surfaces as ClassCastException at the generated cast. Both paths are only reachable when a consumer hand-rolls an ExecutionInput.Builder outside Graphitron.newExecutionInput(…); the typed factory makes the same mistake a compile error at the call site. The framework’s redact path replaces the prose message with a correlation-id reference before it reaches the consumer, so the runtime throw is server-log surface only; the typed factory’s parameter list is the load-bearing diagnostic.
Where the interface comes from
The interface is emitted per app, not imported from a shared module. Every
code-generation run produces one GraphitronContext.java file under
<outputPackage>.schema, written by
GraphitronContextInterfaceGenerator. The generated interface depends only on
graphql-java’s DataFetchingEnvironment and jOOQ’s DSLContext; it does not
pull in any Graphitron runtime jar. The sealed declaration plus nested impl mean
the contract is global to the build (one impl per app), with all per-request
state living in the GraphQLContext the factory populates.
Where each per-request value comes from
Generated fetchers retrieve the singleton through a private helper emitted once per
*Fetchers class:
// GENERATED: from TypeFetcherGenerator.buildGraphitronContextHelper
private static GraphitronContext graphitronContext(DataFetchingEnvironment env) {
return env.getGraphQlContext().get(GraphitronContext.class);
}
The singleton’s default methods then read the per-request values back from
env.getGraphQlContext(): DSLContext.class for the per-request DSLContext,
the contextArgument’s string name for each typed value the factory put. The full
minimum viable wiring (building the schema, wiring GraphQL, calling the factory)
lives in the Runtime API reference.
sequenceDiagram
participant Client
participant Engine as graphql-java engine
participant Fetcher as generated DataFetcher
participant Ctx as GraphitronContext<br/>(consumer impl)
participant DSL as DSLContext
participant DB as PostgreSQL
Client->>Engine: ExecutionInput<br/>(query, populated GraphQLContext, DataLoaderRegistry)
Engine->>Fetcher: invoke (DataFetchingEnvironment env)
Fetcher->>Ctx: graphitronContext(env).getDslContext(env)
Ctx-->>Fetcher: per-request DSLContext<br/>(read from env.getGraphQlContext().get(DSLContext.class))
Fetcher->>DSL: select(...).from(...).where(...)
DSL->>DB: SQL (driven by selection set)
DB-->>DSL: Result<Record>
DSL-->>Fetcher: rows
Fetcher->>Ctx: getContextArgument(env, "userId")<br/>(if @condition uses contextArguments)
Ctx-->>Fetcher: claim value (Java cast applied at call site)
Fetcher-->>Engine: Result<Record> / ConnectionResult
Engine-->>Client: response (graphql-java traverses records via field DataFetchers)
The diagram shows one fetcher invocation; per-request behaviour (tenant routing in getDslContext, session variables for RLS, JWT claims through getContextArgument) all happen on the request thread, scoped to the DataFetchingEnvironment. The generator emits the call sites; the consumer’s GraphitronContext implementation decides what each call returns.
getDslContext: database access
Every generated query method calls getDslContext(env) to obtain the
DSLContext for executing SQL. The default impl reads
env.getGraphQlContext().get(DSLContext.class), populated by the factory’s
defaultDsl parameter. This is the only impl; the sealed interface closes off
ad-hoc overrides. On the escape-hatch path you build the right per-request
DSLContext at request entry and pass it to
Graphitron.newExecutionInput(dsl, …); on the owned-connection path the
instrumentation produces the DSLContext from the pinned connection and
per-tenant routing is the runtime’s Map<TenantId, DataSource> seam
(above). The same shape composes with PostgreSQL
row-level security; see Row-Level Security and the
integrity gradient below.
getContextArgument: passing runtime values into generated methods
getContextArgument passes values from the GraphQL context into generated
condition and method calls. It is invoked when a method parameter is
classified as ParamSource.Context, driven by the contextArguments field
on the @condition, @service, and @tableMethod directives.
For example, a field with
@condition(condition: "AccessControl.visibleToUser",
contextArguments: ["userId"]) produces:
// GENERATED
condition = condition.and(AccessControl.visibleToUser(table,
(String) graphitronContext(env).getContextArgument(env, "userId")));
The Java cast at the generated call site is reflected from the developer
method’s parameter type, so it matches the compile-time signature the consumer
wrote against. The supplying side is the factory’s typed parameter
slot: Graphitron.newExecutionInput(dsl, userId) with userId: String reflects
straight into the consumer’s method, and a typo in the SDL contextArguments:
list surfaces as a compile error at the factory call (not a null at request
time).
Complementary Technologies
The sections below describe standard capabilities that compose naturally with
GraphitronContext. They are not Graphitron-specific extension points: they
work because getDslContext gives you full control over the DSLContext and
its configuration.
Where each concern belongs. Three layers, in order of preference:
-
jOOQ
Configurationfor cross-cutting jOOQ behaviour: type converters, forced types,RecordMapperProvider, naming strategies. Configured once per app and shared by everyDSLContextyou return fromgetDslContext. -
The
DSLContextyou pass intoGraphitron.newExecutionInputfor per-request decisions: which tenant’sDSLContextto use, which session variables toSET LOCAL, which connection to acquire. Anything that varies request-by-request gets composed at request entry and threaded through the factory. -
Schema directives (
@condition,@tableMethod,@reference) for predicates and joins that are part of the schema’s business semantics. Anything a schema author should be able to read in the SDL belongs here, not in a runtime hook.
ExecuteListener is an advanced jOOQ-level hook (logging, metrics, query
rewriting); use it sparingly and prefer Configuration or a directive when
either fits.
Instance @service holders
Instance @service classes are constructed per call via new ClassName(DSLContext).
The holder is created fresh per fetcher invocation; do not stash request-scoped state
on instance fields, since each call gets its own holder. For per-tenant or per-request
decisions inside the holder, ride into the holder via the DSLContext’s
`Configuration: Configuration.data(key) carries ad-hoc per-request values written
by your getDslContext implementation, and Configuration.settings() plus registered
Converter / RecordMapperProvider carry cross-cutting jOOQ behaviour. For
SDL-visible per-request arguments, prefer a static method that takes the value via
getContextArgument. The single-(DSLContext) constructor contract is a deliberate
constraint, mirroring the legacy generator: every per-request seam already exists on
Configuration or in the SDL, so a wider holder ctor would duplicate plumbing
without adding capability.
jOOQ Configuration
For most applications, jOOQ’s Configuration is the most important
extension point. DefaultConfiguration controls type converters, forced
types, synthetic primary keys, embedded records, naming strategies, and
more. These settings flow through every query Graphitron generates.
var config = new DefaultConfiguration();
config.set(SQLDialect.POSTGRES);
config.set(dataSource);
// Type converters, forced types, RecordMapperProvider, etc. go here
config.set(new DefaultRecordMapperProvider());
DSLContext ctx = DSL.using(config);
See the jOOQ Configuration documentation for the full set of options.
jOOQ ExecuteListener
ExecuteListener is an advanced hook that intercepts query execution at
lifecycle points (before rendering, before execution, after execution, etc.).
Most applications do not need this: it is mainly useful for SQL logging,
metrics collection, or query rewriting. Register a listener on
DefaultConfiguration before creating the DSLContext; see the
jOOQ ExecuteListener documentation
for the full lifecycle and available hooks.
Row-Level Security and the integrity gradient
RLS-assumed is a stated principle. Graphitron cannot enforce that the
database enforces row access; it assumes it, and says so. Generated code issues
plain SELECT statements and carries no authorization logic; row-level
security (RLS/VPD) is the recommended enforcement mechanism, filtering rows
transparently from session state with no change to generated queries. Consumers
who do not use RLS remain responsible for exposing only safe surfaces (views,
restricted schemas). If a mounted identity is wrong or absent, the failure is a
data-exposure failure, not a confusing-UI failure, so the safe posture is fail
closed: an RLS policy must treat both a NULL and an empty session variable as
"no identity" and deny.
The integrity gradient. How tamper-resistant the mounted identity is varies by dialect and pattern, and it is a spectrum, not a yes/no. Choosing where on it to sit is the load-bearing security decision.
-
Enforced fence (strongest). Oracle package-bound contexts and RAS: code on the connection cannot set identity state except through a trusted definer-rights package. Configure this with the function-hook
<sessionState>form. -
Convention fence. Plain PostgreSQL GUCs set by the
<variables>sugar (or aSECURITY DEFINERfunction that writes them): any SQL on the connection can overwrite them, so the real guarantee is that Graphitron generates every statement and any@servicecode on the connection behaves. The combination "Postgres<variables>sugar +@servicemethods in the schema" earns a loud build-time note (session-state-convention-fence), because that is the convention fence with consumer code on the connection. -
Cryptographic fence. Available on both dialects through the function-hook form: pass the raw signed token as the claims payload, verify the signature inside the database, and have policies read identity only through verified state, so even arbitrary SQL on the connection cannot forge another user’s identity.
On the owned-connection path
Identity is mounted by your configured <sessionState> connect hook (see
above); your RLS policies read whatever it set. For
the Postgres <variables> sugar the connect hook runs
set_config(name, …, false) per request and the disconnect hook clears it, so
a fail-closed policy reads:
-- ILLUSTRATIVE: PostgreSQL RLS policy, fail-closed on unset/empty identity
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY caller_isolation ON documents
USING (owner_id = NULLIF(current_setting('app.user_id', true), ''));
Generated code is unaware of RLS: it issues plain SELECT statements on the
pinned connection and the database enforces the policies automatically.
On the escape-hatch path
You own the session state: set it on the DSLContext you return before handing
it to Graphitron.newExecutionInput(dsl, …). Hold a single connection for the
request so the SET and every query share it:
// ILLUSTRATIVE: setting session context on the escape-hatch DSLContext
Connection conn = dataSource.getConnection();
DSLContext ctx = DSL.using(conn, SQLDialect.POSTGRES);
ctx.execute("SELECT set_config('app.user_id', ?, true)", userId); // isLocal=true: transaction-scoped
var input = Graphitron.newExecutionInput(ctx, /* contextArguments... */).query(q).build();
Producing the claims payload
On the owned-connection path you pass an opaque claims string to
Graphitron.newOwnedExecutionInput(claims, …); no auth-framework type enters
generated code. From MicroProfile JWT (or any JWS bearer) the two forms come
straight off the injected JsonWebToken, and which you pick is the
integrity-gradient decision:
-
Signed compact token (cryptographic fence): pass
jwt.getRawToken(). Your connect hook verifies the signature and reads claims in-database, so the identity holds even against distrusted SQL on the connection. -
Bare claims JSON (what the
<variables>sugar expects): the payload segment of a JWS already is the claims JSON, so decode it with the JDK alone, no JSON parser:
String claims = new String(
Base64.getUrlDecoder().decode(jwt.getRawToken().split("\\.")[1]),
StandardCharsets.UTF_8);
return Graphitron.newOwnedExecutionInput(claims);
getRawToken() returns the three-segment compact serialization, not JSON;
passing it where claims JSON is expected fails at the hook’s jsonb cast. If
your platform hands you an encrypted token (JWE), the payload segment is
ciphertext; rebuild the claims JSON from the platform’s claim accessors instead.
See also:
-
Runtime API reference:
buildSchema, theGraphitronContextfactory, custom scalars, federation, and context arguments from the consumer side -
Code Generation Triggers: Schema patterns to sealed variants, including the
@condition/@tableMethoddirectives that drivegetContextArgumentcalls -
Security Model: Database-level security philosophy