Compared to ORMs & rules engines
Greffon is not an ORM, so "Greffon vs. X" is mostly a question of which layer each tool owns. This page places it next to the tools people usually arrive from — Prisma, Drizzle, Kysely, TypeORM, MikroORM, and .NET's EF Core — and, because a serializable predicate is also a rule, next to the rules-and-policy tools: JSONLogic, CASL, OPA, Cedar. It is explicit about what Greffon does not do.
The same query, seven ways
A filter, a projection, and an ordering, written in each:
const adults = await db.users
.filter((u) => u.age >= 18 && u.name.startsWith("A"))
.map((u) => ({ id: u.id, name: u.name }))
.orderBy((u) => u.name)
.toArray();var adults = await db.Users
.Where(u => u.Age >= 18 && u.Name.StartsWith("A"))
.Select(u => new { u.Id, u.Name })
.OrderBy(u => u.Name)
.ToListAsync();const adults = await prisma.user.findMany({
where: { age: { gte: 18 }, name: { startsWith: "A" } },
select: { id: true, name: true },
orderBy: { name: "asc" },
});const adults = await db
.select({ id: users.id, name: users.name })
.from(users)
.where(and(gte(users.age, 18), like(users.name, "A%")))
.orderBy(users.name);const adults = await db
.selectFrom("users")
.select(["id", "name"])
.where("age", ">=", 18)
.where("name", "like", "A%")
.orderBy("name")
.execute();const adults = await repo.find({
select: { id: true, name: true },
where: { age: MoreThanOrEqual(18), name: Like("A%") },
order: { name: "ASC" },
});const adults = await em.find(
User,
{ age: { $gte: 18 }, name: { $like: "A%" } },
{ fields: ["id", "name"], orderBy: { name: "asc" } },
);The shapes sort into three families. Prisma, TypeORM, and MikroORM encode the predicate as a data object ({ gte: 18 }, MoreThanOrEqual(18), { $gte: 18 }). Drizzle and Kysely build SQL syntax in TypeScript (gte(users.age, 18), .where("age", ">=", 18)). EF Core and Greffon write an ordinary lambda over the row, which the C# compiler — or Greffon's build plugin — also turns into an expression tree.
The lambda family has one property the other two cannot offer: the predicate is still a function. u => u.age >= 18 && u.name.startsWith("A") runs as-is against plain objects — which is exactly how the memory provider executes it, and why the same query file works under your test runner with no database and no mocks.
What each tool owns
| Queries are written as | Schema & migrations | Change tracking | Writes | Same query on plain arrays | |
|---|---|---|---|---|---|
| Greffon | TS lambdas, reified to trees at build time | none — table/column mapping only | none | none — queries only | yes — the reference provider |
| EF Core (.NET) | C# lambdas, compiled to trees | included | yes — DbContext + SaveChanges | yes | yes — LINQ to Objects |
| Prisma | data objects to a generated client | schema.prisma + prisma migrate | no — stateless client | yes | no |
| Drizzle | SQL-shaped builder over a TS schema | TS schema + drizzle-kit | no | yes | no |
| Kysely | typed SQL builder over an interface | migration runner; types by hand or codegen | no | yes | no |
| TypeORM | find-options objects, or a string query builder | decorators + migrations | no — save()-based persistence | yes | no |
| MikroORM | data objects (Mongo-style operators) | decorators / EntitySchema + migrations | yes — unit of work + identity map | yes | no |
"Plain arrays" means fixture objects with no engine at all — an in-memory SQLite or PGlite still exercises a database. Greffon's memory provider evaluates the same lambda the query was written with, and it is the reference semantics every SQL provider is property-tested against.
The part only expression trees provide
Every tool above turns its query representation into SQL. Greffon's difference is that the representation is a typed, versioned, JSON-serializable tree with a public schema — an artifact, not an implementation detail:
- It crosses process boundaries. The same predicate can travel to a server as a remote filter, sit in a store as an auditable policy rule, or be replayed later —
serialize/deserializeare part of@greffon/tree's contract, wire format versioned like any other format. - Anyone can translate it. A provider is a pure function over a closed grammar (Writing a provider); Postgres and SQLite are the first two targets, not a feature list.
- EF Core has the tree too — but as an in-process .NET object graph with no wire format; serializing one is a third-party exercise. In Greffon, serialization is the point.
Applications is the catalog of what that buys — authorization rules, filters over the wire, rules-as-data, and running a serialized tree directly against plain objects.
Next to rules engines & policy languages
The ORM shelf is only half the comparison: a typed, serializable predicate is also what rules engines and policy tools provide. The same placement exercise, on that shelf:
- JSONLogic / json-rules-engine store a rule as JSON operator objects —
{ "and": [{ ">": [{ "var": "age" }, 18] }] }— untyped, written in a bespoke vocabulary, interpreted at runtime. A Greffon tree is also plain JSON, but it is written as a TypeScript lambda: typed against the row, checked by the compiler, validated against a closed grammar — and the same rule compiles to a SQLWHERE, which a JSONLogic rule cannot do without a hand-written translator. - CASL defines isomorphic abilities — "can
readArticlewhereauthorId = user.id" — with Mongo-style condition objects, and covers both the single-object check and (through its integrations) query filtering. The conditions remain a data DSL: not statically typed against your row types, not a plain function you can call without the library. A tree plays the same role with the predicate written in the language of the codebase, andevaluate,print, and SQL translation come from the one definition. - OPA (Rego) / Cedar are separate policy languages with their own evaluators, tooling, and deployment story — the right call when policy must be owned and distributed outside the application. Greffon rules live inside your TypeScript, typed against your schema, and travel as JSON; there is no second language and no engine process — but also no cross-service policy-distribution machinery beyond the JSON itself.
The Applications page calls this policy-as-expression: typed like code, stored and audited like data, translated like a query.
What Greffon deliberately does not do
Stated plainly, because the table compresses it:
- No writes. There is no
insert,update,delete, orsave. Pair Greffon with whatever owns writes — your driver, Kysely, Drizzle, an ORM. - No schema ownership. Providers take mapping metadata (
{ users: { table: "users" } }); nothing generates or migrates tables. - No connections. Providers take an
executorfunction; pooling, transactions, and configuration stay with your driver. - No change tracker, no identity map, no caches. Rows are plain objects; two queries returning the same row give you two objects.
- Pre-0.1. The tools above have years of production use behind them; Greffon is new, and says so.
Choosing
- You want one tool to own schema, migrations, and writes → Prisma, Drizzle, TypeORM, or MikroORM.
- You want SQL's shape visible in TypeScript → Kysely or Drizzle.
- You are on .NET → EF Core is this whole design, native to the platform.
- You want rules stored as data and edited outside a deploy → JSONLogic is the untyped incumbent; a Greffon tree is the typed version of the same idea, with SQL pushdown included.
- You want policy owned outside the application, distributed across services → OPA or Cedar.
- You want query lambdas that run against fixtures in tests and compile to parameterized SQL in production, and predicates that serialize into trees a provider can translate → Greffon, next to whatever owns your writes.
These compose rather than compete: nothing stops reads through Greffon and writes through the ORM that owns your schema, over the same tables. Where the ideas themselves come from is its own story — The C# lineage.