Endpoint spec reference
Endpoint specs are JSON. The shape is defined and validated by
@persql/endpoint-spec (validateEndpointSpec), used by both the
console editor and the create / update REST routes.
Top-level fields
Section titled “Top-level fields”| Field | Type | Required | Notes |
|---|---|---|---|
slug | string | yes | /^[a-z][a-z0-9-]{0,40}$/ |
name | string | yes | Human label |
description | string | no | Surfaced in docs / OpenAPI / .well-known |
kind | enum | yes | query | mutation | auth_signup | auth_login | ask |
method | enum | yes | GET | POST |
sql | string | yes | Parameterised SQL with ? placeholders only |
input | FieldSpec[] | yes | See below |
output | enum | yes | rows | first_row | rows_written | session | answer |
auth | enum | no | public (default) | session |
rateLimit | object | no | { perMin?, perDay? }, per (endpoint × IP) |
captcha | boolean | no | Require Turnstile token in header |
cors | string[] | no | Allowed origins (exact match) |
authConfig | object | only for auth kinds | { usersTable, emailColumn?, passwordColumn: "password_hash", sessionTtlSec? } |
askConfig | object | only for ask | { allowedTables: string[], context?, maxRows? } |
Output shapes
Section titled “Output shapes”output | Response body |
|---|---|
rows | { columns: string[], rows: unknown[][] } |
first_row | { row: object | null } |
rows_written | { rowsWritten: number } |
session | { token: string, expiresAt: string } (auth kinds only) |
answer | { sql, columns, rows, explanation } (ask kind only) |
FieldSpec
Section titled “FieldSpec”Each entry in input declares one input field:
| Field | Type | Notes |
|---|---|---|
name | string | snake_case, /^[a-z][a-z0-9_]{0,40}$/ — or a $-prefixed session field (see below) |
label | string | Defaults to name |
type | enum | text | email | url | integer | number | boolean | json |
required | boolean | Default false |
pattern | string | Regex source (no slashes), text only |
enum | string[] | Allowed values, text only |
min / max | number | Numeric range, integer / number only |
maxLength | number | String cap |
Session-sourced fields
Section titled “Session-sourced fields”Fields whose name starts with $ are never accepted from public
input. The runtime fills them from the verified session JWT before
binding to SQL:
| Name | Source claim |
|---|---|
$user_id | JWT sub (PK in your users table) |
$user_email | JWT email |
$session_iat | JWT iat (unix seconds) |
Use them to scope mutations and queries to the logged-in user:
SELECT * FROM tasks WHERE user_id = ?-- input: [{ name: "$user_id", type: "integer" }]The endpoint must declare auth: "session" for session fields to be
populated.
Worked examples
Section titled “Worked examples”Read-only list
Section titled “Read-only list”{ "slug": "recent-orders", "name": "Recent orders", "kind": "query", "method": "GET", "sql": "SELECT id, total, status FROM orders ORDER BY created_at DESC LIMIT ?", "input": [{ "name": "limit", "type": "integer", "min": 1, "max": 100, "required": true }], "output": "rows"}Mutation gated by session
Section titled “Mutation gated by session”{ "slug": "create-task", "name": "Create task", "kind": "mutation", "method": "POST", "sql": "INSERT INTO tasks (user_id, title) VALUES (?, ?)", "input": [ { "name": "$user_id", "type": "integer" }, { "name": "title", "type": "text", "required": true, "maxLength": 200 } ], "output": "rows_written", "auth": "session"}Validation errors
Section titled “Validation errors”createEndpoint and updateEndpoint return 400 with an issues
array on validation failure:
{ "success": false, "error": "Validation failed", "issues": [ { "path": "input[0].pattern", "message": "is not a valid regex" } ]}Slug collisions inside the same database return 409.