gqlorm
gqlorm is a Prisma-inspired GraphQL query builder that lets you fetch data
from your Cedar backend using an ORM-style API on the frontend. Instead of
hand-writing GraphQL documents, you write familiar Prisma-like queries and
gqlorm generates the GraphQL for you — complete with live-query support.
// Before: writing GraphQL by hand
const QUERY = gql`
query FindTodos {
todos {
id
title
body
done
}
}
`
// With gqlorm: Prisma-style queries
const { data } = useLiveQuery((db) => db.todo.findMany())
gqlorm is an experimental feature. Enable it in cedar.toml and expect APIs
to evolve as the feature matures.
What gqlorm provides
- Auto-generated GraphQL types and resolvers from your Prisma schema — no manual SDL required for basic CRUD reads
- ORM-style query builder on the frontend:
db.todo.findMany(),db.post.findUnique({ where: { id: 1 } }), etc. - Live queries out of the box via the
useLiveQueryhook, which automatically adds the@livedirective - Automatic auth scoping — queries are scoped to the current user and organization when your schema includes membership fields
- Sensitive-field filtering — fields like
password,secret, andtokenare hidden from the GraphQL API by default
Enabling gqlorm
Add the experimental flag to your cedar.toml:
[experimental.gqlorm]
enabled = true
When you run yarn cedar dev or yarn cedar build, Cedar generates three
artifacts in .cedar/:
| File | Purpose |
|---|---|
.cedar/gqlorm-schema.json | Frontend model schema mapping model names to visible scalar fields |
.cedar/gqlorm/backend.ts | Auto-generated GraphQL SDL and resolvers for the API side |
.cedar/types/includes/web-gqlorm-models.d.ts | TypeScript type declarations for the frontend query builder |
Frontend setup
Import the generated schema and call configureGqlorm once at app startup.
Typically you do this at the top of App.tsx:
import { configureGqlorm } from '@cedarjs/gqlorm/setup'
import schema from '../../.cedar/gqlorm-schema.json' with { type: 'json' }
configureGqlorm({ schema })
configureGqlorm is idempotent and safe to call multiple times. Passing schema lets the query builder know which scalar fields exist for each model, so useLiveQuery((db) => db.todo.findMany()) requests every visible field instead of falling back to id only.
Fetching data with useLiveQuery
useLiveQuery is the primary way to fetch data on the web side. Pass it a query function and it returns { data, loading, error } just like a standard GraphQL query hook — but the query is annotated with @live so it automatically re-fetches when the underlying data changes.
import { useLiveQuery } from '@cedarjs/gqlorm/react/useLiveQuery'
const LiveTodos = () => {
const { data, loading, error } = useLiveQuery((db) => db.todo.findMany())
if (loading) {
return <div>Loading...</div>
}
if (error) {
return <div>Error: {error.message}</div>
}
if (!data || data.length === 0) {
return <div>No todos yet</div>
}
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
export default LiveTodos
Supported query operations
The query function supports the same read operations you know from Prisma:
| Operation | Description | Example |
|---|---|---|
findMany | List all matching records | db.todo.findMany() |
findUnique | Fetch a single record by unique field | db.todo.findUnique({ where: { id: 1 } }) |
findFirst | Fetch the first matching record | db.todo.findFirst({ where: { done: true } }) |
findUniqueOrThrow | Like findUnique but throws if missing | db.todo.findUniqueOrThrow({ where: { id: 1 } }) |
findFirstOrThrow | Like findFirst but throws if missing | db.todo.findFirstOrThrow({ where: { done: true } }) |
findFirst, findUniqueOrThrow, and findFirstOrThrow are client-side abstractions. They generate GraphQL queries against the same singular-model field that findUnique uses. The only difference is how the result is handled: findUnique/findFirst return null when no record matches, while findUniqueOrThrow/findFirstOrThrow throw an error.
Filtering and sorting
You can use where, orderBy, take, and skip just like Prisma:
const { data } = useLiveQuery((db) =>
db.post.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 10,
})
)
Complex where clauses with AND, OR, and operators like gt, contains, etc. are also supported:
const { data } = useLiveQuery((db) =>
db.post.findMany({
where: {
AND: [{ published: true }, { createdAt: { gt: '2024-01-01' } }],
},
})
)
Selecting specific fields
Without an explicit select, useLiveQuery requests every visible scalar field defined in the generated schema. To request only specific fields, pass a select object:
const { data } = useLiveQuery((db) =>
db.todo.findMany({
select: { id: true, title: true },
})
)
Query builder API (advanced)
If you need more control — for example to build a one-off GraphQL document without React — you can use the query builder directly:
import { buildQuery, buildQueryFromFunction } from '@cedarjs/gqlorm'
// Build from model/operation/args
const graphqlQuery = buildQuery('todo', 'findMany', {
where: { done: false },
})
// Build from a query function
const liveQuery = buildQueryFromFunction(
(db) => db.todo.findUnique({ where: { id: 1 } }),
{ isLive: true }
)
Both return a GraphQLQuery object with query (string) and optional variables.
Controlling schema visibility
gqlorm decides which models and fields are exposed through a small set of rules you control with documentation directives in schema.prisma.
Hide a model
Add /// @gqlorm hide above the model to exclude it entirely:
/// @gqlorm hide
model InternalAuditLog {
id Int @id @default(autoincrement())
action String
createdAt DateTime @default(now())
}
Hide or show a field
Add /// @gqlorm hide or /// @gqlorm show above a field:
model User {
id Int @id @default(autoincrement())
email String @unique
/// @gqlorm hide
apiKey String // stays hidden even though it doesn't match the heuristic
/// @gqlorm show
metadata Json // explicitly exposed even if the heuristic would hide it
}
Sensitive-field heuristics
By default, gqlorm hides any scalar field whose lowercased name contains one of these substrings:
password, secret, token, hash, salt, apikey, secretkey, encryptionkey, privatekey
If a field is auto-hidden, Cedar prints a warning at build time telling you how to confirm the hide (/// @gqlorm hide) or override it (/// @gqlorm show).
Auth and multi-tenancy
When your Prisma schema includes a membership model (by default Membership) with userId and organizationId fields, gqlorm automatically scopes generated resolvers:
- User scoping — if a model has a
userIdfield,findManyreturns only rows belonging tocurrentUser.id, andfindUniqueverifies ownership before returning the record. - Organization scoping — if a model has an
organizationIdfield and aMembershipmodel exists,findManyrestricts results to organizations the current user belongs to.
The membership model itself is exempt from organization scoping (it is the source of membership data).
Configuring membership fields
If your schema uses different names, configure them in cedar.toml:
[experimental.gqlorm]
enabled = true
membershipModel = "TeamMember"
membershipUserField = "memberId"
membershipOrganizationField = "teamId"
How the backend works
Cedar generates .cedar/gqlorm/backend.ts during the build. This file:
- Exports a
schemaobject (agqldocument) with GraphQL types for each visible model andQueryfields forfindManyandfindUnique. - Exports a
createGqlormResolvers(db)factory that returns resolver functions wired to your Prisma client.
The frontend query builder supports five operations (findMany, findUnique,
findFirst, findUniqueOrThrow, findFirstOrThrow), but the backend only
needs two GraphQL fields: a plural one (models) for findMany, and a singular
one (model) for the other four. findFirst, findUniqueOrThrow, and
findFirstOrThrow are client-side abstractions — the generated GraphQL queries
hit the same singular-model resolver as findUnique, and only differ in how the
result is handled (returning null vs. throwing).
A Babel plugin injects the db import into api/src/functions/graphql.ts and
passes it to createGqlormResolvers, so the generated resolvers are merged into
your GraphQL schema automatically.
If you already have a manually-written SDL file that defines a type with the
same name (e.g. type Todo { ... } in api/src/graphql/todos.sdl.ts), gqlorm
skips generating that model to avoid duplicate-type errors.
Limitations and known behavior
- Read-only for now — gqlorm currently generates queries (
findMany,findUnique,findFirst, etc.). Mutations (create,update,delete) are not yet auto-generated. - Scalar and enum fields only — relation fields are excluded from the generated schema. You can still use
includein the query builder, but nested relations default to selectingidonly unless the schema is extended. - Live queries require a stateful server — because
@liveuses Server-Sent Events, you cannot use live queries on serverless deploy targets like Netlify or Vercel without additional infrastructure. See the Realtime docs for details. - Experimental flag required — all gqlorm behavior is gated behind
experimental.gqlorm.enabled.
Summary
gqlorm lets you treat your GraphQL API like a Prisma client on the frontend. After enabling the experimental flag and calling configureGqlorm, you can write:
const { data } = useLiveQuery((db) => db.todo.findMany())
and Cedar handles the rest: generating the GraphQL document, keeping it in sync with your schema, scoping it to the current user, and re-fetching automatically when data changes.