# ADR-GQL-001 — GraphQL context stays inbound; object authorization moves to application-core; the persisted-operation store stays an inbound SPI - Status: Accepted - Date: 2026-08-24 - Review: `docs/reviews/2026-08-14-graphql-module-code-review.md` GQL-026 ## Context The GraphQL leaf's own documentation described three things crossing its boundary: a `GraphQlRequestContext` with a deadline propagated into application, JPA, Mongo and the HTTP client; object authorization decided inside the transport; and a persisted-operation registry implemented by an external durable store. Two of those invert the dependency direction. If `application-core` or an outbound adapter implements a type that lives in `adapter:inbound:graphql`, the registry edge that says inbound depends on application is satisfied while the real compile-time dependency runs the other way. The third is a business rule in the wrong layer: whether an actor may see an object is a decision about the domain, and GraphQL is one of four transports this skeleton ships. ## Decision Three different answers, because the three problems are not the same problem. **GraphQL context stays inbound-local.** It is mapped explicitly onto application command fields — actor, tenant, deadline — rather than travelling as a type. Nothing outside the leaf references `GraphQlRequestContext`, and the boundary test is that grep returns nothing outside it. **Object authorization moves to `application-core`.** `ObjectAccessPolicy`, `ObjectAccessRequest` and `ObjectAccessDecision` are transport-neutral and live with the other application policies; `ApplicationObjectAuthorization` in the GraphQL leaf is the bridge that calls them. This is the one of the three that was a real layering defect, and it is fixed rather than documented. **The persisted-operation store stays an inbound-owned SPI.** `GraphQlPersistedOperationRegistry` remains in `advanced/persisted`, and no leaf outside GraphQL implements it. ## Consequences The third decision is the one that needs defending, because it leaves the reported risk in place rather than removing it. The risk is conditional: the direction inverts only when something outside the leaf implements the interface. Nothing does. The template ships an in-memory registry and no durable one, because it ships no persisted-operation store at all. The alternative was to introduce a generic operational key-value store port owned by a neutral contract holder, with the GraphQL adapter owning only the key and value mapping. That port would have exactly one interface, zero implementations and one speculative consumer — a new abstraction whose shape is guessed from a requirement nobody has stated. This repository has spent a full remediation pass deleting controls that existed and were reached by nothing, and inventing a port for a store that does not exist is how the next one of those gets written. So the decision is to leave the SPI where it is and to move it when a durable store is actually built. Moving it then is a rename across one leaf and one new adapter, which is cheaper than carrying a wrong abstraction until then. What must not happen in the meantime is an outbound leaf implementing the inbound interface, because that is the moment the direction actually inverts, and it would happen in a commit whose diff looks like an implementation rather than a layering change. The composition root wires these and owns no business or storage policy of its own. ## Enforcement `verifyCleanArchitectureDependencies` and `modules.json` hold the leaf's edges to `domain-core`, `application-core` and `shared-contract`. `ObjectAccessPolicyTest` covers the application-side policy and `ApplicationObjectAuthorizationTest` the bridge. The condition this ADR turns on — that nothing outside the GraphQL leaf implements the persisted-operation SPI — is a claim about the whole repository, so it is checked at the composition root rather than inside the leaf, next to the other GraphQL boundary rules in `app-bootstrap`'s architecture suite.