## Get the database schema

`GET /v1/infra/servers/:id/db/schema`

Returns the tables and views of the application's SQLite database with columns, indexes, primary keys and a flag showing whether the table's rows can be edited through the API. Use the schema to choose a table for reading rows and to check whether it allows edits.

Database access conditions and request limits — [When the database is available](/docs/infra/database#when-the-database-is-available) and [Limits](/docs/infra/database#limits).

## Parameters

| Parameter | Type | Required | Default | Description |
|----------|-----|:-----:|-----------|----------|
| `id` (path) | string (UUID) | yes | — | ID of an application with `kind` equal to `GALAXY_APP`. List: [`GET /v1/infra/servers`](/docs/infra/servers/list) |
| `file` (query) | string | no | `data.defaultFile` from [`GET /db`](/docs/infra/database/info) | Database file path relative to `/data`, up to 1024 bytes in UTF-8. File list — `data.files[].file` in [`GET /db`](/docs/infra/database/info). An absolute path, `.` and `..` segments and symbolic links are not accepted |

Other query string parameters return `400 VALIDATION_ERROR`.

## Examples

### curl — personal key

```bash
curl "https://vibecode.bitrix24.com/v1/infra/servers/APP_ID/db/schema" \
  -H "X-Api-Key: YOUR_API_KEY"
```

### curl — OAuth application

```bash
curl "https://vibecode.bitrix24.com/v1/infra/servers/APP_ID/db/schema" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

### JavaScript — personal key

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${appId}/db/schema`,
  { headers: { 'X-Api-Key': 'YOUR_API_KEY' } }
)
const { success, data, error } = await res.json()
if (!success) throw new Error(error.code)
for (const t of data.tables) {
  console.log(t.name, t.type, t.editability.editable ? 'editable' : t.editability.reason)
}
```

### JavaScript — OAuth application

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${appId}/db/schema`,
  {
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
    },
  }
)
const { success, data, error } = await res.json()
if (!success) throw new Error(error.code)
console.log(data.tables.map(t => t.name))
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | `true` on success |
| `data.file` | string | Path of the file read, relative to `/data` |
| `data.tables` | array | Tables and views, in alphabetical order. SQLite internal tables with names starting with `sqlite_` are excluded |
| `data.tables[].name` | string | Table or view name. Pass it in `:table` when [reading rows](/docs/infra/database/rows) |
| `data.tables[].type` | string | `table` — a table, `view` — a view |
| `data.tables[].columns` | array | Columns in declaration order, including hidden and generated ones |
| `data.tables[].columns[].name` | string | Column name |
| `data.tables[].columns[].type` | string | Column type as declared in the schema. An empty string if no type is declared |
| `data.tables[].columns[].notNull` | boolean | Whether a `NOT NULL` constraint is declared |
| `data.tables[].columns[].defaultValue` | string \| null | Default value expression as text, for example `CURRENT_TIMESTAMP` or `1`. `null` if there is no default value |
| `data.tables[].columns[].primaryKeyPosition` | integer | Position of the column in the primary key, starting at `1`. `0` — the column is not part of the key |
| `data.tables[].columns[].hidden` | integer | `0` — a regular column, `1` — a hidden column of a virtual table, `2` — a `VIRTUAL` generated column, `3` — a `STORED` generated column |
| `data.tables[].indexes` | array | Table indexes |
| `data.tables[].indexes[].name` | string | Index name |
| `data.tables[].indexes[].unique` | boolean | Whether the index is unique |
| `data.tables[].indexes[].origin` | string | Index origin: `c` — created by `CREATE INDEX`, `u` — by a `UNIQUE` constraint, `pk` — by the primary key |
| `data.tables[].indexes[].partial` | boolean | Whether the index is partial, that is, has a `WHERE` condition |
| `data.tables[].indexes[].columns` | array | Index columns in order. `null` in place of an expression if the index is built on an expression |
| `data.tables[].primaryKey` | array | Primary key columns in order. An empty array if no key is declared |
| `data.tables[].editability.editable` | boolean | Whether rows can be added, updated and deleted through the API |
| `data.tables[].editability.keyKind` | string \| null | How a row is addressed: `pk` — by the primary key, `rowid` — by the SQLite built-in row number. `null` if editing is unavailable |
| `data.tables[].editability.keyColumns` | array | Columns the row key is built from. For `rowid` — a single `rowid`, `_rowid_` or `oid` column. An empty array if editing is unavailable |
| `data.tables[].editability.reason` | string \| null | Why editing is unavailable: `view` — a view, `virtual_table` — a virtual table, `no_stable_key` — there is no primary key and the built-in row number is unavailable: the table is declared `WITHOUT ROWID` or the names `rowid`, `_rowid_` and `oid` are taken by columns. `null` if editing is available |
| `data.tables[].rowCountEstimate` | integer \| null | Row count estimate from SQLite statistics. `null` if there are no statistics |
| `data.truncated` | boolean | `true` if the database has more than 500 tables and views or the schema approaches 1 MiB. The response then contains only some of the tables |

## Response example

```json
{
  "success": true,
  "data": {
    "file": "app.db",
    "tables": [
      {
        "name": "active_customers",
        "type": "view",
        "columns": [
          { "name": "id", "type": "INTEGER", "notNull": false, "defaultValue": null, "primaryKeyPosition": 0, "hidden": 0 },
          { "name": "name", "type": "TEXT", "notNull": false, "defaultValue": null, "primaryKeyPosition": 0, "hidden": 0 }
        ],
        "indexes": [],
        "primaryKey": [],
        "editability": {
          "editable": false,
          "keyKind": null,
          "keyColumns": [],
          "reason": "view"
        },
        "rowCountEstimate": null
      },
      {
        "name": "customers",
        "type": "table",
        "columns": [
          { "name": "id", "type": "INTEGER", "notNull": false, "defaultValue": null, "primaryKeyPosition": 1, "hidden": 0 },
          { "name": "name", "type": "TEXT", "notNull": true, "defaultValue": null, "primaryKeyPosition": 0, "hidden": 0 },
          { "name": "email", "type": "TEXT", "notNull": false, "defaultValue": null, "primaryKeyPosition": 0, "hidden": 0 },
          { "name": "created_at", "type": "TEXT", "notNull": false, "defaultValue": "CURRENT_TIMESTAMP", "primaryKeyPosition": 0, "hidden": 0 }
        ],
        "indexes": [
          { "name": "idx_customers_email", "unique": false, "origin": "c", "partial": false, "columns": ["email"] }
        ],
        "primaryKey": ["id"],
        "editability": {
          "editable": true,
          "keyKind": "pk",
          "keyColumns": ["id"],
          "reason": null
        },
        "rowCountEstimate": null
      },
      {
        "name": "order_items",
        "type": "table",
        "columns": [
          { "name": "order_id", "type": "INTEGER", "notNull": true, "defaultValue": null, "primaryKeyPosition": 1, "hidden": 0 },
          { "name": "line", "type": "INTEGER", "notNull": true, "defaultValue": null, "primaryKeyPosition": 2, "hidden": 0 },
          { "name": "sku", "type": "TEXT", "notNull": false, "defaultValue": null, "primaryKeyPosition": 0, "hidden": 0 },
          { "name": "qty", "type": "INTEGER", "notNull": false, "defaultValue": "1", "primaryKeyPosition": 0, "hidden": 0 }
        ],
        "indexes": [
          { "name": "sqlite_autoindex_order_items_1", "unique": true, "origin": "pk", "partial": false, "columns": ["order_id", "line"] }
        ],
        "primaryKey": ["order_id", "line"],
        "editability": {
          "editable": true,
          "keyKind": "pk",
          "keyColumns": ["order_id", "line"],
          "reason": null
        },
        "rowCountEstimate": null
      }
    ],
    "truncated": false
  }
}
```

## Error response example

409 — a running application has closed its database connection in WAL mode:

```json
{
  "success": false,
  "error": {
    "code": "APP_DB_WAL_CLOSED",
    "message": "A running application must keep its database connection open for reads and copies. A sleeping application can be read and copied with the connection closed.",
    "details": {
      "source": "sqlite",
      "hint": "A running application must keep its database connection open for reads and copies. A sleeping application can be read and copied with the connection closed."
    }
  }
}
```

Retrying does not help until the running application opens the connection again. How to keep the connection open — [When the database is available](/docs/infra/database#when-the-database-is-available).

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `VALIDATION_ERROR` | An unknown parameter was passed, or `file` is empty or longer than 1024 bytes |
| 400 | `APP_DB_INVALID_FILE` | `file` is an absolute path, contains `.` or `..`, goes through a symbolic link or does not point to an SQLite database |
| 400 | `APP_DB_UNSUPPORTED_PLACEMENT` | The application is not hosted in Galaxy |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is missing |
| 401 | `INVALID_API_KEY` | The key is not recognized |
| 402 | `BILLING_FROZEN` | The galaxy host is asleep and the balance is frozen — the host cannot be woken |
| 403 | `INFRA_SCOPE_REQUIRED` | The key lacks the `vibe:infra` scope |
| 403 | `APP_DB_DISABLED` | Database access is not enabled for the portal |
| 403 | `SERVER_WAKE_BLOCKED` | The galaxy host is asleep and waking it is blocked |
| 404 | `NOT_FOUND` | The application does not exist, was deleted or is not accessible to this key |
| 404 | `APP_DB_NOT_FOUND` | There is no `/data` volume or database file — the application has not created a database yet |
| 409 | `APP_DB_WAL_CLOSED` | The database is in WAL mode, but the `-wal` and `-shm` files are missing: a running application has closed the connection. There is no `Retry-After` header |
| 409 | `APP_DB_BUSY` | The host is busy with another operation, did not connect within 10 seconds, or the database is locked by an application write. The response carries a `Retry-After` header |
| 422 | `APP_DB_CORRUPT` | The database integrity check failed |
| 422 | `APP_DB_QUERY_TIMEOUT` | Reading the schema did not finish within 5 seconds |
| 422 | `APP_DB_SQL_ERROR` | A table has more than 256 columns or more than 256 indexes; `error.details.reason` is `other` |
| 429 | `RATE_LIMITED` | The read request limit for the portal was exceeded — [Limits](/docs/infra/database#limits). The pause is in the `Retry-After` header |
| 503 | `APP_DB_TOOL_UNAVAILABLE` | The host is not prepared for database reads or is temporarily unavailable. The response carries a `Retry-After` header. Preparation — [Prepare the host](/docs/infra/database/prepare) |
| 503 | `user_self_deletion_pending` | The key owner is deleting their account on the portal |

Full list of common API errors — [Errors](/docs/errors).

## Known specifics

- **Rows are not counted.** `rowCountEstimate` comes from the `sqlite_stat1` statistics table, which is populated when the application itself runs `ANALYZE`. The call does not count the table's rows, so without statistics the value is `null`, and the estimate lags behind the data by the time since the last `ANALYZE`.

## See also

- [Get database information](/docs/infra/database/info)
- [Get table rows](/docs/infra/database/rows)
- [Run a read-only SQL query](/docs/infra/database/query)
- [Application database](/docs/infra/database)
