Joqi

JSON query compiler for TypeScript backends

JSON queries your backend can trust.

Joqi lets product surfaces send structured JSON for filters, sorting, selected fields, and joins. Your server validates that JSON against a registry, binds params, and compiles the approved query for your database adapter.

dialects
mysql pg sqlite
input
json
output
SQLPlan

server/report-route.ts

const report = {
  version: "v1",
  source: "invoice",
  select: ["number", "total", "customer.name"],
  where: {
    and: [
      { field: "status", op: "eq", value: { $param: "status" } },
      { field: "total", op: "gte", value: { $param: "minTotal" } }
    ]
  },
  limit: { $param: "limit" }
};

const result = await runtime.run({
  spec: report,
  params: { status: "open", minTotal: 10000, limit: 25 },
  explain: true
});

JSON query in

registry validates

trusted plan out

The contract

Product surfaces send JSON. Your backend decides what it means.

1. JSON query from an invoice table or report

query.json

{
  "version": "v1",
  "source": "invoice",
  "select": ["number", "status", "total", "customer.name"],
  "where": {
    "and": [
      { "field": "status", "op": "eq", "value": { "$param": "status" } },
      { "field": "total", "op": "gte", "value": { "$param": "minTotal" } }
    ]
  },
  "orderBy": [{ "field": "total", "direction": "desc" }],
  "limit": { "$param": "limit" }
}

2. Registry policy owned by your backend

registry.ts

const policy = {
  version: "v1",
  sources: {
    invoices: {
      expose: true,
      exposeAs: "invoice",
      fields: {
        number: { expose: true, filterable: true, sortable: true },
        status: { expose: true, filterable: true },
        totalCents: {
          expose: true,
          exposeAs: "total",
          operators: ["eq", "gt", "gte", "lt", "lte"]
        }
      },
      relations: {
        customer: { expose: true, target: "customer", selectable: true }
      }
    }
  }
};

3. Server route binds request params

server.ts

const result = await runtime.run({
  spec: body.query,
  params: {
    status: body.status,
    minTotal: body.minTotal,
    limit: 25
  },
  explain: true
});

return Response.json({ rows: result.rows });

4. Approved query compiles to a SQLPlan

explain.sqlPlan

{
  "dialect": "postgres",
  "sql": "select ... where \"invoices\".\"status\" = $1 and \"invoices\".\"totalCents\" >= $2 limit $3",
  "params": ["open", 10000, 25]
}

Backend control

Adapters run the query. Policies define the surface.

Joqi keeps the core small: validate public JSON, compile a SQLPlan, and hand it to backend-owned execution and policy layers.

Adapters

Execution stays behind your backend.

Use the Drizzle adapter today, or provide one executor function for your database client. Joqi hands over dialect-specific SQL text and bound params.

Drizzle adapter

Bring your own executor

MySQL / PostgreSQL / SQLite

adapter.ts

const runtime = createQueryRuntime({
  db,
  physicalRegistry,
  defaults,
  policy,
  dialect: "postgres",
  executor: drizzleExecutor()
});

Policies

JSON only means what you expose.

The same JSON shape can power invoice tables, exports, and revenue dashboards, but every source, field, relation, operator, and limit is policy-owned.

Expose public names

Restrict operators

Cap limits and relation depth

policy.ts

const policy = {
  sources: {
    invoices: {
      expose: true,
      exposeAs: "invoice",
      fields: {
        number: { expose: true, sortable: true },
        totalCents: {
          expose: true,
          exposeAs: "total",
          operators: ["eq", "gte", "lte"]
        }
      }
    }
  }
};