Persistence
What std.db gives you
- SQLite + Postgres connection, query, transaction, and prepared- statement primitives with typed row decode.
- A migration system with checked-in SQL migrations, drift detection, checksum validation, and CI-safe dry runs.
- An audit-log schema for AI actions: who/what/why, prompt version, model, tool call, approval state, cost, trace id, replay key.
- Encrypted token/credential storage for connector tokens.
Schema and migrations
corvid migrate status # show applied/pending/driftedcorvid migrate up [--dry-run] # apply pending migrationscorvid migrate down [--dry-run] # roll back the last migrationMigration files live in migrations/ with a NNNN_name.up.sql /
NNNN_name.down.sql pair:
migrations/├── 0001_users.up.sql├── 0001_users.down.sql├── 0002_audit_log.up.sql└── 0002_audit_log.down.sqlThe migration state store records applied migrations + checksum.
corvid migrate status flags drift if a migration’s recorded
checksum doesn’t match the file on disk.
Connecting
import "@stdlib/db" as db
@budget($0.001)agent fetch_user(id: String) -> Option<User> uses db_read_effect: let conn = db.connect("postgres://localhost/myapp")? let row = conn.query_one("select id, email from users where id = $1", [id])? match row: Some(r) -> Some(User { id: r.get("id"), email: r.get("email") }) None -> NoneThe connection is pooled. db_read_effect is a stdlib-provided effect
that carries data: external_input and a configurable cost.
Transactions
agent transfer(from_id: String, to_id: String, amount: Cents) -> Result<Unit, String> uses db_write_effect: let conn = db.connect(...) conn.transaction(fn (tx) -> { tx.execute("update accounts set balance = balance - $1 where id = $2", [amount, from_id])? tx.execute("update accounts set balance = balance + $1 where id = $2", [amount, to_id])? return Ok(Unit) })Transactions auto-rollback on Err. Nested transactions are rejected
at compile time.
Typed row decode
struct User: id: String email: String created_at: Timestamp
let users: List<User> = conn.query_typed::<User>( "select id, email, created_at from users limit 10", [])?The compiler generates the per-struct decoder. A row that doesn’t
match the struct shape produces a typed Err(DecodeError) with the
column-name and expected-type annotation, not a runtime panic.
Audit-log schema
corvid migrate apply --include-stdlib-auditAdds the canonical audit-log table:
create table audit_log ( id ulid primary key, occurred_at timestamptz not null default now(), actor text not null, action text not null, prompt_version text, model text, tool_name text, approval_state text, cost_cents int, trace_id text, replay_key text, metadata jsonb);Use db.audit_log.write(...) from agents:
db.audit_log.write({ actor: "system", action: "refund_issued", cost_cents: 5000, trace_id: trace.current_id(), metadata: { customer_id, amount }})Encrypted tokens
let token = db.tokens.get_encrypted("gmail_oauth", customer_id)?let access_token = token.decrypt()? # decrypt with host keyToken values never appear in traces, error messages, or logs. The
runtime redacts them automatically. corvid doctor validates that
the host encryption key is present and well-formed without printing it.