Database
Every workspace runs on a Postgres of its own: one
stackbone devstarts for you on your laptop, and the one you point the deployed box at in production. Your tables live in it next to the box's own. You declare them in TypeScript, the CLI writes the migration, the box applies it when it starts, and your code queries throughstackbone.database, a Drizzle handle already bound to the right connection. Studio's DB Explorer reads the same database with a read-only role, so you can look at the rows without openingpsql.
What lives here
Open DB Explorer under Data and the left column lists three schemas. Only the first one is yours to shape:
| Schema | Written by | What it holds |
|---|---|---|
public |
Your migrations, and the connector broker. | Your tables, the __stackbone_migrations__ journal that records which of your migrations ran, and the two tables the broker keeps for connectors and their grants. |
stackbone_platform |
The box, on its first boot and on every upgrade. | Runs, steps, sessions, approvals and their decisions, prompts and their versions, config versions, guardrails, eval cases, suites and runs, secrets, trigger links. |
stackbone_checkpoints |
The deep-agent runtime. | The saved state of every agent conversation, so a session survives a restart and resumes where it left off. |
You read runs, approvals and the rest here like any other table, which helps
when a screen does not slice the data the way you need. Point the box at an
empty Postgres and the first boot creates the two platform schemas
by itself; the one thing it needs is the pgvector extension available, and
the Compose bundle ships an image that has it.
Declare tables and ship migrations
Tables are TypeScript, in src/schema.ts at the root of your workspace, with
the Drizzle helpers re-exported through @stackbone/sdk/db:
import { integer, pgTable, serial, text, timestamp } from '@stackbone/sdk/db';
export const leads = pgTable('leads', {
id: serial('id').primaryKey(),
email: text('email').notNull(),
company: text('company'),
status: text('status').notNull().default('new'),
score: integer('score').notNull().default(0),
createdAt: timestamp('created_at').defaultNow().notNull(),
});Then two commands:
stackbone db migrate create add_leads # writes .stackbone/migrations/0000_add_leads.sql
stackbone db migrate up # applies what is pending to the database in front of youThe first diffs the schema against the migrations you already have and writes
the next SQL file, plus its journal entry under meta/. Commit both: the
migrations are part of your workspace, and every environment replays them in
order. The second applies the pending ones under an advisory lock and records
each in the journal, so running it twice, or from two processes at once, is
safe. stackbone db migrate status says which files are applied, pending, or
drifted from what the database recorded.
You rarely run up by hand. stackbone dev applies pending migrations before
it brings your code up, and a deployed box does the same on start: the
migrations travel inside the image, the container applies them under the same
lock and journal, and only then serves traffic. A DBA who owns every schema
change sets STACKBONE_SKIP_MIGRATIONS=1 on the container and applies the
files by hand.
Query from your code
Inside a tool or a workflow step, stackbone.database is Drizzle, bound to
the workspace's Postgres. Import your tables and query them; the connection
string never appears in your code:
import { stackbone } from '@stackbone/sdk';
import { eq } from '@stackbone/sdk/db';
import { leads } from '../src/schema';
async function qualifyLead(email: string, score: number) {
'use step';
const [lead] = await stackbone.database
.update(leads)
.set({ status: score >= 70 ? 'qualified' : 'contacted', score })
.where(eq(leads.email, email))
.returning();
if (!lead) throw new Error(`no lead for ${email}`);
return { id: lead.id, status: lead.status };
}The handle opens its connection pool on first use and keeps it for the life of
the process. Wrap several statements in stackbone.database.transaction(...)
when they must land together. A column declared with the vector helper turns
the table into a vector store; the migration adds the extension for you, and
Retrieval builds parsing, chunking and hybrid search on
top of that.
Browse and query from Studio
DB Explorer connects to the same database with a read-only role
(stackbone_viewer) and a five-second statement timeout, so a look at the data
can never change it or hold a lock on a live box.
Browse: pick a table, page through it on its primary key, flip the order.
Browse pages through a table on its primary key, or on a best-effort
order when it has none, and shows each column with its type. Query runs
one SELECT you type, and refuses anything else before it reaches the
database, so a stray DELETE answers with a refusal.
Query: one SELECT, one result, and the role and timeout it ran under printed at the bottom.
The same explorer exists in the terminal: stackbone db schemas lists the
schemas and tables, stackbone db table <schema> <name> pages through one, and
stackbone db query "<sql>" runs a single read. Browsing and querying take the
member role or above; a viewer sees runs and their steps, but not the raw
tables.
Where the data lives
| Environment | Database |
|---|---|
stackbone dev |
A Postgres container with pgvector, started for you, on its own port. stackbone db commands find it on their own while the session runs. |
The folder stackbone package writes |
A Postgres with pgvector in the same Compose file, or any Postgres you name in .env. The box creates its own tables on first boot; your DBA only has to make sure the extension can be created. See what you set on the deployed container. |
| A box you run your own way | DATABASE_URL on the container. |
The control plane never reads this database. Studio's explorer and the CLI talk to the box, and the box talks to its Postgres; runs, sessions, secrets and your rows stay in your infrastructure. One backup of this database therefore covers the whole box.
For your own tests, createTestDatabase from @stackbone/sdk/db/testing
boots a throwaway Postgres in a container, applies your migrations to it and
hands you a Drizzle handle, so a test never touches the database your agent
runs on. See How do I test an agent?.
Read more
stackbone.database: the full tutorial, theagent.yamlsettings, transactions,pgvectorand testing.stackbone db: every migration and explorer command with its flags.- Retrieval: documents, chunks and embeddings on top of the same Postgres.
- Storage: the bucket next to the database, for files rather than rows.