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 DataSource and 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 DSLContext per request and own transaction demarcation and identity yourself; Graphitron’s owned-connection guarantees do not apply. This is the lower-opinion path built on GraphitronContext. 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 argument false means session-scoped), never SET LOCAL and never set_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 of Graphitron.newExecutionInput(dsl, …​). On the owned-connection path you do not supply one, the instrumentation produces it from the pinned connection.

  • Per-request contextArgument values: pass each one as a typed parameter to Graphitron.newExecutionInput(…​). The factory’s parameter list reflects the schema’s declared contextArguments and their reflected Java types, so the consumer’s request-entry code threads each value through a typed slot rather than stashing arbitrary entries on GraphQLContext by 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&lt;Record&gt;
    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&lt;Record&gt; / 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 Configuration for cross-cutting jOOQ behaviour: type converters, forced types, RecordMapperProvider, naming strategies. Configured once per app and shared by every DSLContext you return from getDslContext.

  • The DSLContext you pass into Graphitron.newExecutionInput for per-request decisions: which tenant’s DSLContext to use, which session variables to SET 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 a SECURITY DEFINER function that writes them): any SQL on the connection can overwrite them, so the real guarantee is that Graphitron generates every statement and any @service code on the connection behaves. The combination "Postgres <variables> sugar + @service methods 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, the GraphitronContext factory, custom scalars, federation, and context arguments from the consumer side

  • Code Generation Triggers: Schema patterns to sealed variants, including the @condition / @tableMethod directives that drive getContextArgument calls

  • Security Model: Database-level security philosophy