SUBSTRATEDB

A database for agents.
Built to evolve, made to extend.

See how it works

Why not
just SQL?

You can query Substrate with SQL. The difference is who gets to change the schema, and when.

Read the architecture
Sharing a user
PostgresOne shared table where every ALTER is coordinated, or copies you keep in sync.
SubstrateEach app adds its own tables around one stable ID. Nobody else’s rows change.
Changing the schema
PostgresALTER TABLE, backfills, and queries that suddenly return new columns.
SubstrateInstalls only ever add. Existing queries keep their exact shape.
Agents adding tables
PostgresA migration and a deploy, or an agent holding DROP rights.
SubstrateAgents propose tables in their own namespace. Once approved, they go live.
Access
PostgresRow-level security on shared tables.
SubstrateLinking to Alice and reading what others attached to her are separate grants. Revoking is instant, even mid-query.
Types
PostgresA separate codegen step.
SubstrateTypeScript and Rust clients generated from the schema. Plain SQL still works.

Where it fits.

One agent or fifty, one app or a whole ecosystem. All on one database.

Built to
be extended.

Every app keeps its own tables. They all point at the same people, projects and customers.

One ID everywhere.

A coach and a CRM point at the same Alice. Join their data in one query, if you’re allowed to.

Define tables at runtime.

Agents read the schema, propose a new table, and start writing once it’s approved. No migrations.

Access is granted.

Read, write and link are separate grants. Knowing someone’s ID gives you none of them.

Write it.
Join it.

Give Alice a goal, then read it back with her name. Typed in TypeScript and Rust, or plain SQL.

import { Person, TrainingGoal } from "./generated/schema.js";
import { table, select, eq } from "./sdk/typescript/query.js";

// Give an existing person new, typed memory.
await client.insert(TrainingGoal, {
  person: alice.id,
  value: "Run my first 10K"
});

// Read it back together with their shared identity.
const person = table(Person, "p");
const goal = table(TrainingGoal, "g");

const memory = await select({
  name: person.c.name,
  goal: goal.c.value
})
  .from(person)
  .innerJoin(goal, eq(goal.c.person, person.c.id))
  .where(eq(person.c.id, alice.id))
  .execute(client);

// [{ name: "Alice", goal: "Run my first 10K" }]
Add a goal without changing Person, then join it back with typed fields and inferred results. Assumes the Schema tab is installed, an authorized client, and Alice’s existing identity with no goal yet.

Typed queries ↗
How agents add tables ↗

Same database.
Different access.

Each agent sees only what it’s been granted.

Scoped agentCoachExplicit grants
core.PersonAlice
Read · Reference
fitness.Workout45-minute workout
Read · Write
coach.TrainingGoalFirst 10K
Read · Write
crm.Deal1,250.50
Not granted

Coach can add goals and workouts. CRM’s private deals stay out of reach.

Example grants. Read, write and link rights are granted separately.

Visibility, scopes, and revocation

New tables are readable by any signed-in client by default. Mark one private to limit it to its owner and grantees, or public for anyone. Visibility never grants writes. A scoped credential is a hard limit, and handing it on can only narrow it. Every read and write is checked before results leave or changes commit.

Authorization model

All the
numbers.

One million rows against SQLite and PostgreSQL, on the same CI runner.
Including the queries where we lose.

SQL read benchmarks

26 Oct 2026 · 1 million records · Ubuntu 24.04 runner · µs/query ↓

Substrate SQL resident and SQL paged, SQLite and PostgreSQL: one million records across two namespaces, three rounds on the same GitHub Actions ubuntu-24.04 runner, all results verified. Microseconds per query, median of round means; lower is better.
WorkloadSubstrate SQL residentSubstrate SQL pagedSQLite embeddedPostgreSQL 18.3 · local socket*
Repeated point0.730.341.278.88
Unique equality0.690.511.048.86
Changing parameters, first pass24.3224.00122.1822.14
Same parameters, warm1.912.023.2813.16
Selective join1.050.980.7010.99
Full count/sum1,775117,0167,22529,686
Equality count/sum22.351.200.2511.84
Range aggregate, 1%44.666,04184.23266.72
Broad aggregate, 90%1,480104,6696,31134,174
32 groups4,811121,391366,89445,976
Filtered groups, 50%2,350114,895339,49224,799

*Resident Substrate keeps current rows and indexes in memory; paged Substrate stores them on disk. PostgreSQL includes local IPC and decoding; the other modes are embedded.

Methodology & performance CI

500,000 Person records and 500,000 Score records across two namespaces. Three rounds per mode on the same GitHub Actions ubuntu-24.04 runner, with all results verified. Values are medians of round means, not request p50/p99. Every column measures SQL; native API timings are not shown. Grouping uses 32 buckets, the range aggregate selects 1% of scores, the broad aggregate selects 90%, and filtered grouping selects 50%.

GitHub Actions performance runs

SQL runs on DataFusion, with Substrate’s own indexes and permission checks pushed into the query plan.

How the engine works

Give agents
room to build.

Clone the repo and run the demo: two apps share one database, each with its own tables and permissions.

View on GitHub
npm ci && npm run demo:processes

Substrate is an early prototype. It runs on a single node and isn’t ready for production yet. The SDKs ship from the repository, not npm or crates.io. You’ll need Rust 1.94+ and Node 20+.