GleanQL — TypeScript-Native GraphQL Query Compiler
You write plain React components. GleanQL's compiler reads them at build time
and writes the GraphQL for you — one operation per route, typed, hashed, and
allowlisted. There are no queries, fragments, or useQuery wrappers anywhere
in your app code.
The idea in one screen
Field access is the data requirement:
import { glean } from "@gleanql/client";
import type { Product } from "@gleanql/client/schema";
export default function ProductRoute({ params }) {
const product = glean.product({ handle: params.handle });
return <><ProductHero product={product} /><BuyBox product={product} /></>;
}
function BuyBox({ product }: { product: Product }) {
const price = product.priceRange.minVariantPrice;
return <button>{price.amount} {price.currencyCode}</button>;
}The compiler reads those property accesses across the whole route, following
the value through JSX props into BuyBox. It de-duplicates them and emits one
operation:
query ProductRoute($handle: String!) {
product(handle: $handle) {
__typename
id
title
featuredImage { __typename url }
priceRange {
__typename
minVariantPrice { __typename amount currencyCode }
}
}
}Notice what's not in the component: no fragment, no select block, no
generated ProductRef type. Product looks like the schema type, because it
is one.
Everything else follows the same rule
The read side is half the story. Writes, live data, and re-rendering all keep the same contract — you express intent in plain TypeScript, the build does the GraphQL:
- Mutations are compile-time selectors.
useMutation((m, vars) => m.cartLinesAdd(vars).cart.totalQuantity)becomes a named operation. Its result normalizes into the cache, so every read of the mutated entity updates in place. - Subscriptions compile the same way and stream over SSE or
graphql-ws. Each pushed payload folds into the cache. - Re-rendering is field-grained. The cache versions each record, so a component re-renders only when a record it actually read changes.
- The wire can be locked. Every build emits a sha-256 allowlist of every
operation the app can send. Flip
persisted: trueand only known hashes cross the network.
The task tour walks each of these with running code.
The packages
An app installs two packages — the runtime it imports from, and the build plugin that generates everything into it. The other three are internal building blocks.
@gleanql/client
The runtime you install: cache, Suspense, graph proxies, request scope, transports, the React hooks — plus a generated/ slot for the schema.
@gleanql/vite
The build plugin: provisions @gleanql/client, runs codegen + the compiler, and writes the glean accessor / types / operations into it.
@gleanql/core
Query IR, the q.* builder, the selection merger, the GraphQL printer, schema model, devtools.
@gleanql/compiler
Backend seam + a typescript backend, and the analyzer that extracts reads & prop flow.
How a build works
Every generated operation is validated against the real schema with graphql-js, and the whole pipeline is locked by ~400 tests — including golden fixtures run through two type-checker engines.
Where to go
- Get started — install two packages, point the plugin at your schema, write a component. Five steps, no GraphQL.
- Using GleanQL — the task tour: read, mutate, paginate, subscribe, go optimistic, lock down the wire.
- vs Relay & gqty — where GleanQL sits: gqty's developer experience with Relay's runtime characteristics.
- Architecture & pipeline — the worked example, stage by stage, for the internals.
Three bootable examples live in the repo, and none commit any generated glue:
examples/rwsdk-real— a RedwoodSDK storefront: islands, live SSE prices, persisted mode, a typed registered operation, and the jsdom test harness.examples/rwsdk-todo— TodoMVC on a SQLite Durable Object, with optimistic membership.examples/remix-real— the same data layer on React Router 7. Isomorphic SSR, no RSC.