---
title: "Run SQL on the project schema"
method: POST
path: "/projects/v1/{project_id}/sql"
tags: ["Projects"]
---

# Run SQL on the project schema

`POST /projects/v1/{project_id}/sql`

## Request

- Base URL: `https://api.run402.com`
- URL: `https://api.run402.com/projects/v1/{project_id}/sql`
- Auth: one of:
  - HTTP bearer
  - API key in header `SIGN-IN-WITH-X`
  - HTTP bearer
  - HTTP bearer

Executes SQL (DDL/DML/SELECT) against the project's own schema with the project service key; the path's project must be the key's (403 otherwise). This is the route behind `run402 projects sql`, the SDK's `projects.sql`, and in-function `adminDb().sql()`. A pattern blocklist rejects platform-reaching statements. The structured migration path is POST /apply/v1/plans (spec.database): a schema change made here on a project with a live release carries warning SCHEMA_CHANGE_OUTSIDE_MIGRATION. Several statements in one text body run in one call and carry MULTI_STATEMENT_TEXT_BODY; POST /projects/v1/{project_id}/sql/batch states each statement's result and the transaction explicitly. Lifecycle-gated for non-active projects. A SIWX wallet (SIGN-IN-WITH-X), a sign-in session bearer, or a grant key bearer is also accepted, authorized by the caller's own authority on the project (no service key): project.database.read (org role viewer or higher, or a grant / grant key carrying database:read) and project.database.write (developer or higher, or database:write, which also reads). The credential's shape picks the path; a principal call never returns a key and is audited (with a SHA-256 of the request, never its text) before it runs. A non-member, or a principal without the authority, gets 403 FORBIDDEN with next_actions (ask an org owner or admin for a role, ask for a grant with the capability, or import the service key), and the same answer whether or not the project exists. A principal without write authority, a device-grade sign-in session, and any request with read_only: true (service key included) run read-only, enforced by PostgreSQL: one statement (params allowed) on the project's own database role inside a READ ONLY transaction whose snapshot is taken first, with a 15 s statement timeout; any write, DDL or SET TRANSACTION READ WRITE is refused by the engine with 403 DATABASE_READ_ONLY (a role switch with 403 TENANT_SQL_BOUNDARY) (details.required_role, next_actions), and a result over 10 MB answers 413 SQL_RESULT_TOO_LARGE. A read-write principal call behaves like a service-key call, including its lifecycle, archive and demo guards. The response shape is the same for every caller.

## Path parameters

- `project_id` string, required: Run402 project ID. Public IDs are not UUIDs.

## Request body

- object
  - `sql` string, required: SQL statement(s) to execute
  - `params` unknown[]: Positional parameters for $1, $2, …
    - unknown
  - `read_only` boolean: Run this call read-only (engine-enforced: one statement, READ ONLY transaction, 15 s timeout); a write answers 403 DATABASE_READ_ONLY. Honored for every caller, service key included. Never raises a caller's authority.

## Response `200`

SQL executed

- object
  - `status` string
  - `schema` string
  - `rows` object[]
  - `row_count` integer: Rows returned or affected by the last statement: the statement whose rows and fields are returned.
  - `statements` object[]: One entry per statement, in order. For a multi-statement batch, each statement's own command and count; their sum is the batch total.
    - `command` unknown, required
    - `row_count` integer, required
  - `warnings` object[]: Warnings that never change the result: ROUTE_RETIRING (the caller used a route being retired; next_actions names the successor or the upgrade), SCHEMA_CHANGE_OUTSIDE_MIGRATION (DDL on a project with a live release, which the release no longer describes; next_actions: create_migration), MULTI_STATEMENT_TEXT_BODY (several statements in one text body; use the batch route).
    - `code` 'ROUTE_RETIRING' | 'SCHEMA_CHANGE_OUTSIDE_MIGRATION' | 'MULTI_STATEMENT_TEXT_BODY', required
    - `severity` string
    - `message` string, required
    - `next_actions` object[], required
  - `fields` object[]: Result column descriptors of the last statement.
    - `name` string
    - `type` string

## Other responses

- `401`: Missing or invalid authentication. Run402-originated JSON bodies use the canonical ErrorEnvelope.
- `403`: SQL outside the project's own schema (TENANT_SQL_BOUNDARY), a blocked statement pattern, or a past_due/frozen/dormant project (control-plane gated)
- `413`: SQL_RESULT_TOO_LARGE: a read-only principal result over 10 MB. Add a LIMIT.

---

[API](https://skmtc.dev/run402/apis/run402-api.md) · [All operations](https://skmtc.dev/run402/apis/run402-api/llms.txt) · [OpenAPI document](https://skmtc.dev/run402/apis/run402-api/revisions/893607557f50?raw)
