Vex · data

The query is a document.

You send Vex what you want to know and the shape you want back. A model writes the query once, as a small JSON document. Vex checks it against the schema it read from the database, compiles it to SQL with every value as a parameter, and Postgres answers. The next time the question comes, nobody writes anything.

pnpm add @niscorp/vex @niscorp/prism @niscorp/strata zod · 0.4.2 · Apache-2.0

Postgres is starting in this page and loading the theatre’s bookings. It is the real database, compiled to run in a browser, so the first time takes a few seconds. Nothing is sent to a server.

The vocabulary

Six words.

IntentWhat the caller wants to know, in words. Only the one who writes the query reads it.
ShapeAn example of what should come back. Empty strings and zeros say the types, and a Prism mapping turns the rows into it.
FingerprintWhat a stored query is replayed by: a name you choose, or one Vex mints. A replay asks no model and runs with the values you send.
ContextThe caller’s values. A query names one by its key, and it is bound as a parameter.
ScopeThe server’s values: who is asking, and which rows are theirs. A request cannot set them and a model never sees them.
WriteA write is authored by a developer and stored. It is replayed by name with values. No model writes one.
In a Nisc app

A screen reads by name.

An action loads what it shows with one endpoint declaration: a stored name, and the values that change. There is no handler behind it and no SELECT in the app’s code. A GET on the endpoint lists every stored name, what it takes and what it returns, read off the documents themselves.

The action’s endpoint, whole
coming: {
  url: '/api/vex',
  method: 'POST',
  request: {
    fingerprint: 'boxoffice/coming',
    context: { night: '2026-10-25' },
  },
  target: 'coming',
}
The engine

A closed grammar, and a compiler.

A query is a JSON object in a grammar of nine keys. The grammar is a Zod schema with a description on every field, and the JSON Schema made from it is what a model is handed. The engine takes two optional hooks, generateDsl and mapToShape. Without them it replays what is stored and generates nothing.

0keys in the grammar. No other parses.
0write operations. Each is authored; none is generated.
0model call for the question above. A replay makes none.
0rows at most for a query a model wrote. 0 when it names no limit.
Scope

One query. Two members. Two answers.

A stored query belongs to no caller. A policy says, per table, which rows are whose. Vex places it into the statement after the document is read, bound to values the server knows and the request does not carry. A request that leaves them out is refused: omitting the values means nobody, never everybody.

Postgres is starting in this page and loading the theatre’s bookings. It is the real database, compiled to run in a browser, so the first time takes a few seconds. Nothing is sent to a server.

Three more

The worst a query can do is be refused.

A model can write a query that is wrong, and a caller can send a value that is hostile. Vex takes each against the schema first, and only then to Postgres.

Postgres is starting in this page and loading the theatre’s bookings. It is the real database, compiled to run in a browser, so the first time takes a few seconds. Nothing is sent to a server.

The fingerprint

Stored under a name.

Every stored query has a fingerprint, and a request that carries one replays it. When a query is generated, Vex makes the fingerprint, fp_ and a hash, and returns it with the first answer; the query under it never changes. When you seed a query, you give it the name, and a request cannot replace what you seeded.

A fingerprint Vex made
A name you gave

What is kept is the query and its mapping, never the rows. A migration that changes the schema throws the entry out, and a seeded one is seeded again at boot.

Questions for Vex

What a database engineer asks.

On hearing that a model writes the queries. Twenty-one answers, each the package’s own.

Do I write SQL?NoA request is an intent and a shape, or a stored name and its values.
Does the model write SQL?NoIt writes a JSON document in a closed grammar. The compiler writes the statement.
Do I need a model at all?NoSeed the documents yourself and lock the endpoint. Then nothing generates.
Can it inject anything?NoText in a document reaches the statement as a resolved column, a quoted literal or a quoted output name. A request’s values bind as parameters.
SELECT *?NoEvery column is named. A column the schema does not have does not compile.
Does it write the joins?YesAlong the foreign keys it read from the database. Two tables with no key between them are refused.
Can I paginate?YesWith a cursor the caller sends. There is no offset.
Is there row-level security?YesIn the engine, on every run, from a policy the model never sees.
No caller named. Does it read everything?NoIt is refused with missing_scope.
Can the model talk its way around the policy?NoThe policy is applied after the model is done, and the model’s tools run as the caller.
Does the model run on every request?NoOnce for a new question. The answer is kept under a fingerprint and replayed.
Is what it wrote tested first?YesCompiled and run as the caller, with a limit of five, before the agent may finish.
Can it still write a wrong query?YesIt runs and shows the wrong rows, nothing worse: it cannot read what the caller may not. Fix it once, and it is stored and replayed from then on.
Can the model write to my database?NoThere is no generation path for writes. A write is authored, linted at seed, and replayed by name.
Can a caller forge their own identity?NoIdentity columns are written from the server’s values, over whatever the document says.
Does it cache rows?NoIt caches the plan: the document, its mapping, the schema’s fingerprint. Rows are read on every replay.
Does a schema change break stored queries?NoAn entry that no longer matches the schema is evicted on read, and a seeded one is seeded again at boot.
Can a query keep answering?YesMark it refresh: 'reactive' and follow it. It answers again when a table it reads is written.
Does every call cost tokens?NoGeneration costs tokens once, for a new question. A replay costs none.
Postgres only?YesOne adapter today, and all the SQL is in it. An adapter for another database is small to write. Open an issue on GitHub if you need one.
Does it avoid N+1?YesA query is one statement. A correlated condition is exists, compiled inline, so a list of ids is never fetched and sent back.