Add a store
Use this when a bag needs to keep data between calls: notes, a queue, a
cache. A bag declares a SQLite store in bag.yaml, writes its schema as SQL
migrations, and opens the store with openBagStore from
@barry-rocks/sdk/stores. Barry places the file, backs it up with the rest of
its stores, and keeps one per machine or one per identity. The example is
notes, a bag with tools that add and list notes.
Before you begin
Section titled “Before you begin”- Create a bag with a
tools.ts. - Decide who owns the data. A store
owned_by: nodeholds one copy for the machine. A storeowned_by: identityholds one copy for each identity that packs the bag.
Declare the store
Section titled “Declare the store”name: notesdescription: Short notes the agent can add and list, kept in the bag's own SQLite store.
stores: notes: owned_by: node engine: sqlite migrations: ./migrations/notesEach key under stores: names one store, in lowercase letters, digits,
hyphens, and underscores. Every store needs all three fields: owned_by is
node or identity, engine is sqlite, and migrations names a directory
inside the bag. A bag with one store can write store: with the same three
fields instead; Barry names that store default. A manifest can’t have both.
Write the migrations
Section titled “Write the migrations”Each migration is a file named NNN_description.sql, numbered from 001
with no gaps. Migrations only go forward: add a new file for each change, and
never edit one that has shipped.
CREATE TABLE notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')));The description after the number uses lowercase letters, digits, hyphens, and underscores, and starts with a letter or digit. A file that holds only comments or whitespace fails as an empty migration.
Barry runs every pending migration in one transaction, so a migration can’t
contain BEGIN, COMMIT, END, ROLLBACK, SAVEPOINT, RELEASE,
VACUUM, ATTACH, or DETACH, except for BEGIN and END inside a
CREATE TRIGGER body. Up and down sections aren’t supported. Barry records each migration’s name and a checksum of its SQL, and
refuses a store whose history doesn’t match.
Carry the migrations in code
Section titled “Carry the migrations in code”Barry bundles the bag before it runs, and the bundle can’t read files beside the bag’s source. So the bag carries the same SQL as data:
import type { Migration } from "@barry-rocks/sdk/stores";
/** * The SQL under migrations/notes, carried in code because Barry bundles the * bag and a bundle cannot read files beside its source. Keep each entry * byte-for-byte equal to its file: Barry checksums both. */export const MIGRATIONS: Migration[] = [ { name: "001_initial.sql", sql: `CREATE TABLE notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')));`, },];Barry migrates the store from the files, and the bag migrates it from this list, so both must hold the same SQL to the byte. A test that compares them, covered in Test a bag, catches a drift before Barry does.
Open the store
Section titled “Open the store”openBagStore opens the file, sets busy_timeout, write-ahead logging, and
foreign keys, then applies any pending migrations. It opens lazily, on the
first call to db():
import { join } from "node:path";import Database from "better-sqlite3";import { bagDataDir } from "@barry-rocks/sdk/services";import { migratedStorePath, openBagStore } from "@barry-rocks/sdk/stores";import { MIGRATIONS } from "./migrations.js";
export type NotesDb = Database.Database;
const store = openBagStore({ file: "notes.db", migrations: MIGRATIONS, connect: (path) => new Database(path), scratchGuard: "notes", path: () => process.env.BARRY_NOTES_DB ?? migratedStorePath("notes", "notes", () => join(bagDataDir("notes"), "notes.db"), "notes", "node"),});
export const getDb = (): NotesDb => store.db();/** Test seam: use this handle instead. It is migrated first. */export const setDb = (db: NotesDb): void => store.replace(db);export const closeDb = (): void => store.close();The options that matter most:
| Option | What it does |
|---|---|
file |
The store’s filename, used in migration errors |
migrations |
The list from src/migrations.ts |
path |
Where the file lives, asked on every open |
connect |
Opens the file with the bag’s SQLite driver |
identityScoped |
Keeps one handle per identity, for an owned_by: identity store |
scratchGuard |
Under vitest, refuses a path outside the temp directory |
migratedStorePath returns the store’s place in Barry’s active store
generation, the one backups and restores carry, and calls the fallback you
pass when the installation has none. Its last argument is the owner, node
here. For an identity-owned store, pass identity and set
identityScoped: true.
Use better-sqlite3, as Barry’s own stores do: every connection to one file
within a process must use the same SQLite library. better-sqlite3 is a
native addon, so Barry’s build never bundles it. It’s one of the build’s
default externals, and at run time the bag loads the copy Barry links from
its own installation, so every bag and Barry share one SQLite library. The
bag’s own copy serves its types, its editor, and its tests. Pin the same
versions of better-sqlite3 and @types/better-sqlite3 that Barry’s SDK
pins, so the tests run against the library the bag runs on:
{ "name": "notes-bag", "private": true, "type": "module", "scripts": { "test": "vitest run" }, "dependencies": { "@barry-rocks/sdk": "link:../barry/sdk", "better-sqlite3": "12.10.0", "zod": "4.4.3" }, "devDependencies": { "@types/better-sqlite3": "7.6.13", "vitest": "4.1.0" }}Read and write through the store
Section titled “Read and write through the store”Keep the queries in one module, and keep the tools thin:
import { getDb } from "./db.js";
export interface Note { id: number; text: string; created_at: string;}
export function addNote(text: string): Note { return getDb().prepare("INSERT INTO notes (text) VALUES (?) RETURNING *").get(text) as Note;}
export function listNotes(limit = 20): Note[] { return getDb().prepare("SELECT * FROM notes ORDER BY id DESC LIMIT ?").all(limit) as Note[];}import { Barry } from "@barry-rocks/sdk";import { z } from "zod";import { addNote, listNotes } from "./src/store.js";
export const add = Barry.Tool({ namespace: "notes", name: "add", access: "write", description: "Add a short note.", schema: { text: z.string().min(1).describe("The note") }, handler: async ({ text }) => addNote(text),});
export const list = Barry.Tool({ namespace: "notes", name: "list", access: "read", description: "List the newest notes first.", schema: { limit: z.number().int().positive().max(100).optional().describe("How many to show") }, handler: async ({ limit }) => listNotes(limit),});Verify
Section titled “Verify”Install and pack the bag, grant its trait to the identity as Create a bag describes, then add a note and list it:
barry install ~/repos/notesbonanza pack notesbonanza identity traits add notesbonanza tool notes add "buy milk"bonanza tool notes listThe last command prints the note as JSON, with its id and created_at. To
see the store file, run bonanza store list. Its entry reads
[node] notes:notes, with the file’s size and ver: 1, the number of
migrations applied.
If it fails
Section titled “If it fails”- The store reports a checksum mismatch or a renamed migration. The SQL in
src/migrations.tsdiffers from its file, or a shipped migration changed. Make the two match, and add a new migration for the change instead. - The store is at a version this build doesn’t know. A newer build of the bag migrated it. Install that build again; Barry refuses to let older code read a newer schema.
- A test refuses to open the store. The test points the store at its real
location. Set
BARRY_NOTES_DBto a temp path, or callsetDbwith an in-memory database. - For anything else, see Troubleshooting.
What’s next
Section titled “What’s next”- Test a bag to run the store against a temp file.
- Add a service to serve the store’s data over HTTP.
- Services and stores for where stores live and how Barry backs them up.
- The
/storesSDK reference for every export.