How a request flows
Every POST /v1/query goes through the same stages, in order. A failure at
any stage stops the request there, and every outcome is written to the
audit log.
client ──HTTP──▶ curral ├─ authenticate Basic (bcrypt) · API keys · OIDC/JWT ├─ queue global and per-user concurrency limits ├─ inspect DuckDB's own planner: statement type, tables read and written ├─ authorize embedded OPA policy (Rego) → allow / deny ├─ protect rewrite the query with row filters and column masks └─ execute & stream JSON · CSV · NDJSON · Arrow IPC1. Authenticate
Section titled “1. Authenticate”The Authorization header picks the method: a local user (Basic), a service
API key or an OIDC token. The result is a user name and a list of roles.
Repeated failures lock the IP or user name out. See
Users and API keys and OIDC.
Failure: 401, or 429 during a lockout.
2. Queue
Section titled “2. Queue”At most --max-concurrency queries execute at once. A request waits up to
--queue-timeout for a slot. Per-user quotas keep one user from taking every
slot. See Limits and fairness.
Failure: 503 (queue full) or 429 (user over quota).
3. Inspect
Section titled “3. Inspect”The statement is prepared, not executed. curral reads DuckDB’s
unoptimized logical plan to learn the statement type, the base tables read
(views expanded) and the table functions used, and a tokenizer finds the
objects written. When something cannot be determined with certainty,
resolved is false. See Policy input.
Failure: 400 for invalid SQL. Requests with more than one statement are rejected before anything runs.
4. Authorize
Section titled “4. Authorize”The inspection result, the user and their roles go to the embedded OPA
policy, which must return allow. Optional rules return per-request limits
and column masks. Some statements (ATTACH, LOAD, INSTALL…) are denied
whatever the policy says. See Writing a policy.
Failure: 403.
5. Protect
Section titled “5. Protect”If the user has row filters or column masks on any table read, each reference to that table is replaced by a subquery that filters and masks first. Queries that cannot be rewritten safely are denied.
Failure: 403 with decided_by: protection.
6. Execute and stream
Section titled “6. Execute and stream”The query runs on a fresh connection, inside one transaction that also covered inspection and authorization. Results stream as JSON, CSV, NDJSON or Arrow IPC, cut at the row limit in effect.
Failure: 504 on timeout; an error after streaming started arrives in the
X-Curral-Error trailer. See HTTP API.