jevql serve and the protocol
The local HTTP transport the SDKs use, and the JSON documents it speaks.
jevql serve runs the same executor as the CLI behind a tiny JSON API. It is what the TypeScript and Python SDKs talk to. By default they start a private one themselves; run it by hand when you want one shared engine, one connection and one warm cache for a whole team.
jevql serve --listen 127.0.0.1:7433 --token secret
Connection and TypeSafe settings come from the usual flags, environment variables and config file. --token (or JEVQL_TOKEN) requires Authorization: Bearer <token> on every request.
Endpoints
GET /v1/health
{"ok": true, "version": "0.1.0", "model": "jev-latest"}
POST /v1/query
{"sql": "SELECT name FROM people WHERE jev(people, 'could work from home')", "threshold": 0.7, "explain": false, "max_rows": 500}
Only sql is required. threshold and max_rows override the server defaults for that request. explain: true returns the plan without calling TypeSafe.
QueryResult
{
"columns": ["name", "p"],
"rows": [["Ada Fernandes", 0.93], ["Miguel Costa", 0.94]],
"row_count": 2,
"tag": "SELECT 2",
"jev": true,
"stats": {"collect_rows": 6, "judged": 6, "requests": 1, "cache_hits": 0, "input_tokens": 1200, "output_tokens": 40, "usd": 0.00005, "elapsed_ms": 1350},
"explain": null
}
tag is the Postgres command tag, so row-less statements return INSERT 0 1 or CREATE TABLE with empty columns. stats is null for plain SQL. Values use the same canonical encoding as the cache: numbers as numbers, timestamps as RFC 3339 strings, bytea as base64, json columns as nested JSON.
More endpoints
The complete contract is served by every engine at GET /openapi.json as an OpenAPI 3.1 document. In short:
POST /v1/explain takes the same body as /v1/query and returns the Explain object only. No TypeSafe calls.
POST /v1/judge judges rows you already hold, with no database involved:
{"question": "is about billing", "kind": "noul", "rows": [{"subject": "Charged twice"}, {"subject": "API returns 500"}]}
{"answers": [{"p": 0.93, "pass": true, "confidence": 0.93}, {"p": 0.04, "pass": false, "confidence": 0.96}], "stats": {"judged": 2, "requests": 1, "cache_hits": 0, "usd": 0.00002, "...": "..."}}
kind is noul (default), choice with options, or score with ordered options; answers then carry choice and probabilities, or score and norm. Identical rows are judged once, and answers share the cache with SQL queries. --max-rows and --max-chars apply.
GET /v1/cache and DELETE /v1/cache report and clear the answer cache. They return 403 unless the server enables cache admin.
GET /v1/schema/tables lists user relations; GET /v1/schema/tables/{name} describes one, with columns and indexes, like \d.
Errors
{"error": "relation \"nope\" does not exist", "code": "sql"}
| Status | Code | When |
|---|---|---|
| 400 | sql |
parse, rewrite or Postgres error |
| 402 | budget |
--max-rows or --max-chars fired; nothing was sent to TypeSafe |
| 502 | api |
TypeSafe returned an error |
| 401 | auth |
bad or missing bearer token |
| 403 | auth |
feature disabled on this server (cache admin) |
| 404 | sql |
relation not found (schema endpoints) |
How the SDKs embed it
An SDK starts jevql serve --listen 127.0.0.1:0 --token <random> --ready-json --parent-pid <its pid>. The engine picks a free loopback port, prints one JSON line with the address, and exits by itself if the host process disappears. The SDK finds the binary via JEVQL_ENGINE_PATH, its bundled platform package, or PATH. Full contract in sdk/PROTOCOL.md.
The same documents from the CLI
jevql --json-table -c "SELECT 1; SELECT name FROM people WHERE jev(people, 'x')"
prints one QueryResult per statement, one per line. Errors go to stderr as text, with exit code 1 for SQL and 2 for API or budget errors. The SDKs’ CLI transport wraps exactly this, which is handy for scripts and notebooks where you do not want a server.