Skip to content

Database

Database stores every Thing in an owner database per namespace, with an analytics copy of every Event and reads at any past moment.

Database stores every Thing a Startup owns in db.sb and serves it through one typed grammar, $.Noun.verb. Relationships, all-or-nothing writes, analytics, past states and migrations use the same grammar.

Contract

A Noun defines a type, and a Thing is one instance of it. Declaring a Noun gives it typed create, get, list, update and delete on every Interface. Every write runs as an Action attributed to its Worker.

GuaranteeDetail
One recordEach namespace has one owner database with a single writer; a read after Thing.update sees the write
TypedTypes generate from the Nouns, so a renamed property fails at compile time, never in production
RelationshipsDeclared once on the Nouns, written with Thing.relate and Thing.unrelate, and filtered from either end
Find or createThing.resolve returns found, created or held; two matches write nothing and open a merge Task
All or nothingWrites inside one $.transaction commit together as one Action; a throw cancels every write in it
AnalyticsEvery Action and Event lands in an append-only analytics store that Collection.query reads with SQL
Past statesNothing is overwritten: get with at returns a Thing as it stood, and listing Actions by Action.thing returns every write to it
MigrationsA Noun changes only through a Change; Change.review classifies it before Change.merge
IsolationNo query crosses Startups; each Customer's records sit in its Tenant
ScaleOne owner database carries 500 to 1,000 requests a second; past that, a Collection shards by key under the same ids

Reads

A list returns one page, newest first, with a cursor. It filters by any property or Relationship of the Noun.

TypeScript
// Leaseline tracks lease renewals. It declares one Noun, Lease.
import { $ } from 'startup-builder'

const page = await $.Lease.list({ where: 'status = active and endsOn < 2027-03-01', sort: 'endsOn', limit: 50 })
const next = await $.Lease.list({ where: 'status = active', after: page.after })

Relationships

A Relationship has a name from each end. Leaseline declares that a Contact rents a Lease, read back as rentedBy.

TypeScript
await $.Thing.relate({ thing: 'contact_7', predicate: 'rents', to: 'lease_42' })
const rented = await $.Lease.list({ where: 'rentedBy = contact_7' })

Find or create

TypeScript
const { thing, decision } = await $.Thing.resolve({ noun: 'customer', match: { email: 'ana@example.com' }, set: { name: 'Ana Costa' } })
// decision is 'found', 'created' or 'held'

All or nothing

TypeScript
await $.transaction(async (tx) => {
  const lease = await tx.Lease.create({ unit: '4C', endsOn: '2027-06-30' })
  await tx.Thing.relate({ thing: 'contact_7', predicate: 'rents', to: lease.$id })
}) // one Action; a throw before the end cancels both writes

Analytics

Collection.query reads the analytics store with SQL, so a year of rows never slows a write.

TypeScript
const { rows } = await $.Collection.query({ query: "select date_trunc(month, endsOn) as month, count(*) as due from Lease where status = 'active' group by month" })

Past states

TypeScript
const then = await $.Lease.get({ id: 'lease_42', at: '2026-10-01T00:00Z' })
const writes = await $.Action.list({ where: 'thing = lease_42' })

On the API, @ and a time or Version name the same read: GET /leases/lease_42@2026-10-01.

Migrations

To add a property, edit nouns/Lease.mdx and open a Change. Change.review classifies the diff as additive or breaking; a breaking Change opens a new Version, and pinned callers keep the old one.

A type conversion runs as a background Action, and rows that fail it are set aside, never dropped. Compatibility lists every class.

Verbs

Interfaces

Example

Terminal
# Leaseline counts the leases ending each month, as JSON for a Dashboard.
npx startup-builder do Collection.query --json '{"query":"select date_trunc(month, endsOn) as month, count(*) from Lease group by month"}'