--- title: 'db' description: 'Run the migration engine against your workspace database and browse a running installation read-only' position: 18 --- # stackbone db > This page has two halves with two different targets. The Explorer verbs follow > the usual [target resolution](/docs/cli/reference/conventions#target-resolution) and > [pagination](/docs/cli/reference/conventions#pagination) rules; every verb here emits the > shared [JSON envelope](/docs/cli/reference/conventions#json-output) under `--json`. The `stackbone db ...` family runs the **same** migration engine that [`stackbone dev`](/docs/cli/reference/dev) runs on boot, so you can drive it from the shell. `migrate up` is advisory-locked and atomic per file: a failed file rolls back on its own and leaves the journal at the last good entry. The journal records every applied tag. The two halves target different databases. The `migrate` verbs run the migration engine against the **workspace database** (the connection comes from `STACKBONE_POSTGRES_URL`, or is discovered from a running `stackbone dev` session for this project) and they take no `--agent`. The read-only Explorer verbs (`query`, `schemas`, `table`) talk to a **running installation's** runtime and do take `--agent`, like the rest of the runtime surface. By default the schema lives at `src/schema.ts` and migrations under `.stackbone/migrations`. A workspace's agents and workflows all talk to the one install Postgres, so they share this root schema. An optional root `agent.yaml` can override either path (`database.schema`, `database.migrations`). See [Project manifests](/docs/cli/reference/agent-yaml). `agent.yaml` is optional here: with none present the migrate verbs use the convention defaults. The verbs that talk to the database (`migrate up` and `migrate status`) read the connection string from `STACKBONE_POSTGRES_URL` when it is set. If it is not set, they discover it from a running `stackbone dev` session for this project: `dev` records its database connection while it runs, so you do **not** have to export the variable in a separate shell. With neither the variable set nor a live `dev` session the command exits `3` (no project). | Command | Purpose | | ------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `stackbone db migrate up` | Apply every pending migration (advisory-locked, atomic per file). Supports `--target `. | | `stackbone db migrate create ` | Generate a new SQL migration by diffing `src/schema.ts` against the journal. | | `stackbone db migrate status` | Classify each migration as `applied`, `pending` or `drifted` (informational). | | `stackbone db query [sql]` | Run one read-only `SELECT` against the installation. SQL from the positional, `--file ` or stdin. | | `stackbone db schemas` | List the schemas and tables visible to the installation, with row estimates. | | `stackbone db table ` | Browse rows of one table with cursor pagination (`--limit`, `--cursor`, `--order asc\|desc`). | ## stackbone db migrate up Apply every pending migration under `.stackbone/migrations` against the workspace database. Uses an advisory lock so concurrent invocations are safe, and the journal records each applied tag. Takes no `--agent`. | Flag | Type | Description | | ---------- | ------ | ---------------------------------------------------------------------------------------- | | `--target` | string | Stop after applying the migration with this tag (inclusive). Useful for staged rollouts. | **JSON payload** ```jsonc { "schema_version": 1, "applied": [{ "tag": "0001_init", "appliedAt": "2026-05-16T10:00:00Z" }], "skipped": [{ "tag": "0000_bootstrap", "appliedAt": "2026-05-15T09:00:00Z" }], } ``` `appliedAt` is best effort: it comes from a second read of the journal. If that read does not come back, the command emits the entry without the field rather than failing. **Exit codes**: `0` ok, `3` no project (no database connection, `STACKBONE_POSTGRES_URL` unset and no running `stackbone dev` session to discover it from), `1` generic (migration failure, atomic per-file rollback already ran). ## stackbone db migrate create `stackbone db migrate create ` generates a new SQL migration by diffing `src/schema.ts` against the migrations journal. Wraps `drizzle-kit generate` with an in-memory config so you do not need a `drizzle.config.ts` in the project. It never opens a database connection, so it works with no `STACKBONE_POSTGRES_URL` and no running `stackbone dev`. Takes no `--agent`. The `name` positional becomes the migration filename suffix. Must match `^[a-z0-9]+(?:_[a-z0-9]+)*$` (lowercase letters, digits and single underscores, e.g. `add_users`, `add_users_email`). Renames need an interactive terminal. A rename looks like one table or column removed and another added. The migration engine cannot tell a rename from a drop-and-create, so it asks you to confirm. Run `stackbone db migrate create` in your terminal (without `--json` or `--yes`) so you can answer that prompt. With `--json`, with `--yes`, in CI, or under the `stackbone dev` auto-migrate watcher there is no one to answer. The command then fails with a message telling you to re-run it in your terminal instead of writing no migration. **JSON payload** ```jsonc { "schema_version": 1, "migration_file": ".stackbone/migrations/0002_add_users.sql", } ``` **Exit codes**: `0` ok, `4` not found (the resolved schema file does not exist on disk), `1` generic (invalid name, `agent.yaml` parse error, drizzle-kit failure). ## stackbone db migrate status Classify each migration as `applied`, `pending` or `drifted` (informational only: it reports drift instead of failing on it). Takes no `--agent`. **JSON payload** ```jsonc { "schema_version": 1, "entries": [ { "tag": "0000_bootstrap", "state": "applied", "appliedAt": "2026-05-15T09:00:00Z" }, { "tag": "0001_init", "state": "pending" }, ], } ``` **Exit codes**: `0` ok (regardless of drift), `3` no project, `1` generic (cannot reach the DB). ## stackbone db query `stackbone db query [sql]` runs one ad-hoc read-only `SELECT` against the targeted installation. The SQL comes from the positional argument, then `--file `, then stdin, in that precedence order. | Flag | Type | Description | | --------- | ------ | ----------------------------------------------------------------------------------------- | | `--agent` | string | Installation id to target. Defaults to the local-dev installation linked to this project. | | `--file` | string | Read the SQL from this file instead of the positional argument. | The installation's runtime enforces a single `SELECT` and rejects anything else server-side. It runs the statement as a `stackbone_viewer` role inside a transaction with a statement timeout. It also caps results: the runtime wraps a query that does not end in its own `LIMIT` at 1000 rows, and sets `truncated: true` when the query hits that ceiling. **JSON payload** ```jsonc { "schema_version": 1, "columns": [ { "name": "id", "data_type": "integer" }, { "name": "email", "data_type": "text" }, ], "rows": [{ "id": 1, "email": "a@b.com" }], "truncated": false, "duration_ms": 12, } ``` **Exit codes**: `0` ok · `3` no project or `dev_not_running` · `1` generic (no SQL provided, unreadable or empty `--file`, non-SELECT SQL rejected by the runtime). ## stackbone db schemas List the schemas and tables visible to the targeted installation, with cheap row estimates. | Flag | Type | Description | | --------- | ------ | ----------------------------------------------------------------------------------------- | | `--agent` | string | Installation id to target. Defaults to the local-dev installation linked to this project. | **JSON payload** ```jsonc { "schema_version": 1, "schemas": [ { "name": "public", "tables": [{ "name": "users", "estimated_rows": 1200 }], }, ], } ``` `estimated_rows` is `null` when the runtime has no estimate for that table. **Exit codes**: `0` ok · `3` no project or `dev_not_running` · `1` generic. ## stackbone db table `stackbone db table
` browses rows of one table with cursor pagination. Both positionals must be unquoted Postgres identifiers (start with a letter or underscore, then letters, digits or underscores, max 63); the CLI validates them before making the network call. | Flag | Type | Description | | ---------- | ------ | ----------------------------------------------------------------------------------------- | | `--agent` | string | Installation id to target. Defaults to the local-dev installation linked to this project. | | `--cursor` | string | Opaque cursor from a previous page (`nextCursor`). | | `--limit` | number | Maximum number of rows to return (1-200, default 50). | | `--order` | enum | Sort order on the cursor column: `asc` or `desc` (default `asc`). | **JSON payload** Each column carries full metadata here, more than the name and type `db query` returns. ```jsonc { "schema_version": 1, "columns": [ { "name": "id", "data_type": "integer", "is_nullable": false, "is_primary_key": true, "ordinal_position": 1, }, ], "rows": [{ "id": 1, "email": "a@b.com" }], "cursor_column": "id", "nextCursor": "eyJpZCI6MX0", "prevCursor": null, } ``` `cursor_column` names the primary-key column the cursor encodes. It is `null` when the table has no single-column primary key and the browse fell back to `LIMIT`/`OFFSET` pagination, so the order is best effort. **Exit codes**: `0` ok · `3` no project or `dev_not_running` · `1` generic (bad schema or table identifier, non-integer `--limit`, `--order` other than `asc` or `desc`).