SUBSTRATEDB

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

See how it works

New memory.
Same database.

Agents learn things your schema never planned for.

Usually that ends up in a JSON column nobody can query. In Substrate, the agent defines a real table for it: typed, permissioned, and joined to your existing users while the app keeps running.

Read the architecture

Watch an agent
remember.

A coaching agent learns Alice’s goal. It adds a table for goals and links it to her.

Coaching agent

“Remember that I’m training for my first 10K.”

core.PersonAliceSame identity
fitness.Workout45 minExisting record
crm.Deal1,250.50Existing record

One new table. Alice’s other records don’t move.

An agent can add a goal to Alice’s existing identity.

What changes in the database?

The agent proposes a new table in the namespace it’s allowed to use. Depending on your policy, it’s approved automatically or by you. Goals then point at Alice’s existing ID. Person and Workout aren’t touched, because nothing was altered: a table was added.

How proposals are approved

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.

Where it fits.

A few things people build on one shared database.

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+.