Database

Every workspace runs on a Postgres of its own: one stackbone dev starts 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 through stackbone.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 opening psql.

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:

src/schema.ts
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 you

The 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, the agent.yaml settings, transactions, pgvector and 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.
BUILT WITH ❤️ FROM CANADA AND SPAIN