superapp
ReferenceHTTP API

POST /data

Query endpoint request and response format.

The /data endpoint is the single entry point for all data operations. It receives parameterized SQL + params from the Drizzle Proxy client and returns query results. Every request must include a valid JWT.

Request Format

The client sends parameterized SQL built by Drizzle ORM on the client side:

POST /data HTTP/1.1
Content-Type: application/json
Authorization: Bearer <jwt_token>

{
  "sql": "SELECT \"id\", \"amount\", \"status\" FROM \"main\".\"orders\" WHERE \"status\" = $1 ORDER BY \"created_at\" DESC LIMIT $2",
  "params": ["active", 50],
  "method": "all"
}

Request Body Fields

FieldTypeRequiredDescription
sqlstringYesParameterized SQL query built by Drizzle ORM. Uses $1, $2, etc. as placeholders.
paramsany[]YesParameter values corresponding to the SQL placeholders.
methodstringYesQuery execution method. One of: all (returns rows as arrays), get (single row), values (raw values), or run (execute without returning rows).

Request Examples

SELECT (method: "all")

{
  "sql": "SELECT \"id\", \"amount\", \"status\", \"created_at\" FROM \"main\".\"orders\" WHERE \"status\" = $1 AND \"amount\" >= $2 ORDER BY \"created_at\" DESC LIMIT $3 OFFSET $4",
  "params": ["active", 100, 20, 0],
  "method": "all"
}

SELECT single row (method: "get")

{
  "sql": "SELECT \"id\", \"amount\", \"status\" FROM \"main\".\"orders\" WHERE \"id\" = $1 LIMIT $2",
  "params": ["ord_abc123", 1],
  "method": "get"
}

INSERT (method: "run")

{
  "sql": "INSERT INTO \"main\".\"orders\" (\"amount\", \"status\", \"notes\") VALUES ($1, $2, $3) RETURNING \"id\", \"amount\", \"status\", \"created_at\"",
  "params": [250, "draft", "New order"],
  "method": "run"
}

UPDATE (method: "run")

{
  "sql": "UPDATE \"main\".\"orders\" SET \"status\" = $1, \"notes\" = $2 WHERE \"id\" = $3 RETURNING \"id\", \"status\", \"updated_at\"",
  "params": ["active", "Approved", "ord_abc123"],
  "method": "run"
}

DELETE (method: "run")

{
  "sql": "DELETE FROM \"main\".\"orders\" WHERE \"id\" = $1",
  "params": ["ord_abc123"],
  "method": "run"
}

COUNT (method: "get")

{
  "sql": "SELECT count(*) AS \"count\" FROM \"main\".\"orders\" WHERE \"status\" = $1",
  "params": ["active"],
  "method": "get"
}

Aggregation (method: "all")

{
  "sql": "SELECT \"status\", sum(\"amount\") AS \"total\", count(*) AS \"count\", avg(\"amount\") AS \"average\" FROM \"main\".\"orders\" WHERE \"status\" = $1 GROUP BY \"status\"",
  "params": ["active"],
  "method": "all"
}

Response Format

The response follows the Drizzle Proxy protocol. The format depends on the method field in the request.

method: "all"

Returns rows as arrays of values:

{
  "rows": [
    ["ord_001", 250, "active", "2025-03-15T10:00:00Z"],
    ["ord_002", 100, "active", "2025-03-14T09:00:00Z"]
  ]
}

method: "get"

Returns a single row as an array:

{
  "rows": ["ord_001", 250, "active"]
}

method: "run"

Returns an empty result (used for writes):

{
  "rows": []
}

Drizzle ORM on the client maps these raw arrays back into typed objects automatically.

Authentication

Every request to /data must include a valid JWT in the Authorization header:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

If the token is missing, invalid, or expired, the server responds with 401 Unauthorized:

{
  "error": "UNAUTHORIZED",
  "message": "Invalid or expired token.",
  "correlation_id": "req_xyz789"
}

Error Format

All errors follow the same shape:

interface ErrorResponse {
  /** Error code for programmatic handling */
  error: string
  /** Human-readable error message */
  message: string
  /** Unique request ID for log correlation */
  correlation_id: string
  /** Additional details (development mode only) */
  details?: Record<string, any>
}

Error Codes

HTTP StatusError CodeDescription
400BAD_REQUESTInvalid request body, missing required fields, or malformed SQL.
401UNAUTHORIZEDMissing, invalid, or expired JWT token.
403PERMISSION_DENIEDThe user does not have permission for this table/operation.
404TABLE_NOT_FOUNDThe specified table does not exist in any connection.
408QUERY_TIMEOUTThe query exceeded the configured timeout.
413PAYLOAD_TOO_LARGEThe request body exceeds limits.maxRequestBodySize.
422VALIDATION_ERRORWrite data failed the permission's check validation.
429RATE_LIMITEDThe user or IP has exceeded the rate limit.
500INTERNAL_ERRORAn unexpected server error occurred.

Error Example

{
  "error": "PERMISSION_DENIED",
  "message": "You do not have permission to perform this action.",
  "correlation_id": "req_abc123"
}

In development mode (NODE_ENV !== 'production'), the response includes additional details:

{
  "error": "PERMISSION_DENIED",
  "message": "No SELECT permission on table 'main.orders' for role 'viewer'.",
  "correlation_id": "req_abc123",
  "details": {
    "table": "main.orders",
    "operation": "select",
    "role": "viewer",
    "evaluated_permissions": ["view_own_orders"]
  }
}

cURL Examples

# SELECT query
curl -X POST http://localhost:3001/data \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "sql": "SELECT \"id\", \"amount\", \"status\" FROM \"main\".\"orders\" LIMIT $1",
    "params": [10],
    "method": "all"
  }'

# INSERT query
curl -X POST http://localhost:3001/data \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "sql": "INSERT INTO \"main\".\"orders\" (\"amount\", \"status\") VALUES ($1, $2) RETURNING *",
    "params": [250, "draft"],
    "method": "run"
  }'

# COUNT query
curl -X POST http://localhost:3001/data \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "sql": "SELECT count(*) AS \"count\" FROM \"main\".\"orders\" WHERE \"status\" = $1",
    "params": ["active"],
    "method": "get"
  }'

On this page