Skip to content

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.

FieldTypeRequiredNotes
slugstringyes/^[a-z][a-z0-9-]{0,40}$/
namestringyesHuman label
descriptionstringnoSurfaced in docs / OpenAPI / .well-known
kindenumyesquery | mutation | auth_signup | auth_login | ask
methodenumyesGET | POST
sqlstringyesParameterised SQL with ? placeholders only
inputFieldSpec[]yesSee below
outputenumyesrows | first_row | rows_written | session | answer
authenumnopublic (default) | session
rateLimitobjectno{ perMin?, perDay? }, per (endpoint × IP)
captchabooleannoRequire Turnstile token in header
corsstring[]noAllowed origins (exact match)
authConfigobjectonly for auth kinds{ usersTable, emailColumn?, passwordColumn: "password_hash", sessionTtlSec? }
askConfigobjectonly for ask{ allowedTables: string[], context?, maxRows? }
outputResponse 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)

Each entry in input declares one input field:

FieldTypeNotes
namestringsnake_case, /^[a-z][a-z0-9_]{0,40}$/ — or a $-prefixed session field (see below)
labelstringDefaults to name
typeenumtext | email | url | integer | number | boolean | json
requiredbooleanDefault false
patternstringRegex source (no slashes), text only
enumstring[]Allowed values, text only
min / maxnumberNumeric range, integer / number only
maxLengthnumberString cap

Fields whose name starts with $ are never accepted from public input. The runtime fills them from the verified session JWT before binding to SQL:

NameSource claim
$user_idJWT sub (PK in your users table)
$user_emailJWT email
$session_iatJWT 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.

{
"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"
}
{
"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"
}

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.