turso Documentation

A SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.

24,821 stars Rust Rust 1 file · ~23,814 tokens #database#embedded-database#sql#sqlite3#webassembly
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy turso on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name turso -p 8080:8080 --restart always tursodatabase/turso:latest

1. Project Overview & Quickstart (tursodatabase/turso)

File: bindings/rust/README.md

turso

The next evolution of SQLite: A high-performance, SQLite-compatible database library for Rust

Features

  • SQLite Compatible: Similar interface to rusqlite with familiar API apart from using async Rust
  • High Performance: Built with Rust for maximum speed and efficiency
  • Async/Await Support: Native async operations with tokio support
  • In-Process: No network overhead, runs directly in your application
  • Cross-Platform: Supports Linux, macOS, and Windows
  • Transaction Support: Full ACID transactions with rollback support
  • Prepared Statements: Optimized query execution with parameter binding
  • Cloud Sync: Sync with Turso Cloud using Builder::new_remote() (optional sync feature)

Installation

Add this to your Cargo.toml:

toml
[dependencies]
turso = "0.4.3"
tokio = { version = "1.0", features = ["full"] }

For cloud sync capabilities, enable the sync feature:

toml
[dependencies]
turso = { version = "0.4.3", features = ["sync"] }
tokio = { version = "1.0", features = ["full"] }

Quick Start

In-Memory Database

rust
use turso::Builder;

#[tokio::main]
async fn main() -> turso::Result<()> {
    // Create an in-memory database
    let db = Builder::new_local(":memory:").build().await?;
    let conn = db.connect()?;

    // Create a table
    conn.execute(
        "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)",
        ()
    ).await?;

    // Insert data
    conn.execute(
        "INSERT INTO users (name, email) VALUES (?1, ?2)",
        ["Alice", "[email protected]"]
    ).await?;

    conn.execute(
        "INSERT INTO users (name, email) VALUES (?1, ?2)", 
        ["Bob", "[email protected]"]
    ).await?;

    // Query data
    let mut rows = conn.query("SELECT * FROM users", ()).await?;
    
    while let Some(row) = rows.next().await? {
        let id = row.get_value(0)?;
        let name = row.get_value(1)?;
        let email = row.get_value(2)?;
        println!("User: {} - {} ({})", 
            id.as_integer().unwrap_or(&0), 
            name.as_text().unwrap_or(&"".to_string()), 
            email.as_text().unwrap_or(&"".to_string())
        );
    }

    Ok(())
}

File-Based Database

rust
use turso::Builder;

#[tokio::main] 
async fn main() -> turso::Result<()> {
    // Create or open a database file
    let db = Builder::new_local("my-database.db").build().await?;
    let conn = db.connect()?;

    // Create a table
    conn.execute(
        r#"CREATE TABLE IF NOT EXISTS posts (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            content TEXT,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        )"#,
        ()
    ).await?;

    // Insert a post
    let rows_affected = conn.execute(
        "INSERT INTO posts (title, content) VALUES (?1, ?2)",
        ["Hello World", "This is my first blog post!"]
    ).await?;

    println!("Inserted {} rows", rows_affected);

    Ok(())
}

Synced Database

Sync your local database with Turso Cloud:

rust
use turso::sync::Builder;

#[tokio::main]
async fn main() -> turso::Result<()> {
    // Create a synced database
    let db = Builder::new_remote("local.db")
        .with_remote_url("libsql://your-database.turso.io")
        .with_auth_token("your-token")
        .build()
        .await?;

    let conn = db.connect().await?;

    // Create a table and insert data
    conn.execute(
        "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, content TEXT)",
        ()
    ).await?;

    conn.execute(
        "INSERT INTO notes (content) VALUES (?1)",
        ["My first synced note"]
    ).await?;

    // Push local changes to remote
    db.push().await?;

    // Pull remote changes to local
    db.pull().await?;

    Ok(())
}

API Reference

Builder

Create a new database:

rust
let db = Builder::new_local(":memory:").build().await?;
let db = Builder::new_local("data.db").build().await?;

Connection

Execute queries and statements:

rust
// Execute SQL directly
let rows_affected = conn.execute("INSERT INTO users (name) VALUES (?1)", ["Alice"]).await?;

// Query for multiple rows
let mut rows = conn.query("SELECT * FROM users WHERE age > ?1", [18]).await?;

// Prepare statements for reuse
let mut stmt = conn.prepare("SELECT * FROM users WHERE id = ?1").await?;
let mut rows = stmt.query([42]).await?;

// Execute prepared statements
let rows_affected = stmt.execute(["Alice"]).await?;

Working with Results

rust
use futures_util::TryStreamExt;

let mut rows = conn.query("SELECT name, email FROM users", ()).await?;

while let Some(row) = rows.try_next().await? {
    let name = row.get_value(0)?.as_text().unwrap_or(&"".to_string());
    let email = row.get_value(1)?.as_text().unwrap_or(&"".to_string());
    println!("{}: {}", name, email);
}

Sync API Reference

sync::Builder

Create a synced database that synchronizes with Turso Cloud:

rust
use turso::sync::Builder;

let db = Builder::new_remote("local.db")       // Local database path (or ":memory:")
    .with_remote_url("libsql://db.turso.io")   // Remote URL (https://, http://, or libsql://)
    .with_auth_token("your-token")              // Authorization token
    .bootstrap_if_empty(true)                   // Download schema on first sync (default: true)
    .with_remote_encryption("base64-encoded-key", RemoteEncryptionCipher::Aes256Gcm) // Optional remote encryption
    .build()
    .await?;

sync::Database

Operations for synced databases:

rust
// Push local changes to remote
db.push().await?;

// Pull remote changes (returns true if changes were applied)
let had_changes = db.pull().await?;

// Force WAL checkpoint
db.checkpoint().await?;

// Get sync statistics
let stats = db.stats().await?;
println!("Received: {} bytes", stats.network_received_bytes);
println!("Sent: {} bytes", stats.network_sent_bytes);
println!("WAL size: {} bytes", stats.main_wal_size);

License

MIT

Support


File: bindings/react-native/README.md

Turso Sync React Native SDK

React Native bindings for Turso embedded replicas - sync your local SQLite database with Turso cloud.

Installation

bash
npm install @tursodatabase/sync-react-native

iOS

bash
cd ios && pod install

Android

Requires minSdkVersion 21+ in android/build.gradle.

Quick Start

typescript
import { Database, getDbPath } from '@tursodatabase/sync-react-native';

// Get platform-specific writable path
const dbPath = getDbPath('myapp.db');

// Create database with sync
const db = new Database({
  path: dbPath,
  url: 'libsql://your-db.turso.io',
  authToken: 'your-auth-token',
});

// Connect (bootstraps from remote if empty)
await db.connect();

// Query local replica (fast)
const users = await db.all('SELECT * FROM users');

// Make local changes
await db.run('INSERT INTO users (name) VALUES (?)', ['Alice']);

// Sync with remote
await db.push();  // Push local changes
await db.pull();  // Pull remote changes

// Close when done
await db.close();

Local-Only Database

typescript
const db = new Database({ path: getDbPath('local.db') });
await db.connect();

await db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
await db.run('INSERT INTO users (name) VALUES (?)', ['Bob']);
const user = await db.get('SELECT * FROM users WHERE id = ?', [1]);

await db.close();

Encrypted Remote Database

typescript
const db = new Database({
  path: getDbPath('encrypted.db'),
  url: 'libsql://your-db.turso.io',
  authToken: 'your-auth-token',
  remoteEncryption: {
    cipher: 'aes256gcm',
    key: 'base64-encoded-key',
  },
});

API

Database Methods

Method Description
connect() Open/bootstrap the database
exec(sql) Execute SQL (no results)
run(sql, params?) Execute SQL, return { changes, lastInsertRowid }
get(sql, params?) Query single row
all(sql, params?) Query all rows
prepare(sql) Create prepared statement
close() Close database

Sync Methods (when `url` is provided)

Method Description
push() Push local changes to remote
pull() Pull remote changes to local
sync() Push then pull
stats() Get sync statistics

Transactions

typescript
await db.transaction(async () => {
  await db.run('INSERT INTO users (name) VALUES (?)', ['Alice']);
  await db.run('INSERT INTO users (name) VALUES (?)', ['Bob']);
  // Commits on success, rolls back on error
});

License

This project is licensed under the MIT license.


File: bindings/python/README.md


About

Turso Database runs in production today at multiple organizations. It has not yet reached 1.0, so as with any database we recommend keeping backups — see the FAQ for where the project stands.

Features

  • SQLite compatible: SQLite query language and file format support (status).
  • In-process: No network overhead, runs directly in your Python process
  • Cross-platform: Supports Linux, macOS, Windows
  • Remote partial sync: Bootstrap from a remote database, pull remote changes, and push local changes when online — all while enjoying a fully operational database offline.
  • Asyncio support: Built-in integration with asyncio to ensure queries won’t block your event loop

Installation

bash
uv pip install pyturso

Database driver

A minimal DB‑API 2.0 example using an in‑memory database:

python
import turso

# Standard DB-API usage
conn = turso.connect(":memory:")
cur = conn.cursor()

cur.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT)")
cur.execute("INSERT INTO users VALUES (1, 'alice'), (2, 'bob')")

cur.execute("SELECT * FROM users ORDER BY id")
rows = cur.fetchall()
print(rows)  # [(1, 'alice'), (2, 'bob')]

conn.close()

Database driver (asyncio)

Non-blocking access with asyncio:

python
import asyncio
import turso.aio

async def main():
    # Connect and use as an async context manager
    async with turso.aio.connect(":memory:") as conn:
        # Executes multiple statements
        await conn.executescript("""
            CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT);
            INSERT INTO t(name) VALUES ('alice'), ('bob');
        """)

        # Use a cursor for parameterized queries
        cur = conn.cursor()
        await cur.execute("SELECT COUNT(*) FROM t WHERE name LIKE ?", ("a%",))
        count = (await cur.fetchone())[0]
        print(count)  # 1

        # JSON and generate_series also available
        cur = conn.cursor()
        await cur.execute("SELECT SUM(value) FROM generate_series(1, 10)")
        print((await cur.fetchone())[0])  # 55

asyncio.run(main())

Synchronization driver

Use a remote Turso database while working locally. You can bootstrap local state from the remote, pull remote changes, and push local commits.

Note: You need a Turso remote URL. See the Turso docs for provisioning and authentication.

python
import turso.sync

# Connect a local database to a remote Turso database
conn = turso.sync.connect(
    ":memory:",                          # local db path (or a file path)
    remote_url="https://<db>.<region>.turso.io"  # your remote URL
)

# Read data (fetched from remote if not present locally yet)
rows = conn.execute("SELECT * FROM t").fetchall()
print(rows)

# Pull new changes from remote into local
changed = conn.pull()
print("Pulled:", changed)  # True if there were new remote changes

# Make local changes
conn.execute("INSERT INTO t VALUES ('push works')")
conn.commit()

# Push local commits to remote
conn.push()

# Optional: inspect and manage sync state
stats = conn.stats()
print("Network received (bytes):", stats.network_received_bytes)
conn.checkpoint()  # compact local WAL after many writes

conn.close()

Partial bootstrap to reduce initial network cost:

python
import turso.sync

conn = turso.sync.connect(
    "local.db",
    remote_url="https://<db>.<region>.turso.io",
    # fetch first 128 KiB upfront
    partial_sync_experimental=turso.sync.PartialSyncOpts(
      bootstrap_strategy=turso.sync.PartialSyncPrefixBootstrap(length=128 * 1024),
    ),
)

Synchronization driver (asyncio)

The same sync primitives, but fully async:

python
import asyncio

async def main():
    conn = await turso.aio.sync.connect(":memory:", remote_url="https://<db>.<region>.turso.io")

    # Read data
    rows = await (await conn.execute("SELECT * FROM t")).fetchall()
    print(rows)

    # Pull and push
    await conn.pull()
    await conn.execute("INSERT INTO t VALUES ('hello from asyncio')")
    await conn.commit()
    await conn.push()

    # Stats and maintenance
    stats = await conn.stats()
    print("Main WAL size:", stats.main_wal_size)
    await conn.checkpoint()

    await conn.close()

asyncio.run(main())

License

This project is licensed under the MIT license.

Support


File: bindings/javascript/docs/API.md

class Database

The Database class represents a connection that can prepare and execute SQL statements.

Methods

new Database(path, [options]) ⇒ Database

Creates a new database connection.

Param Type Description
path string Path to the database file
options object Options.

The path parameter points to the SQLite database file to open. If the file pointed to by path does not exists, it will be created.
To open an in-memory database, please pass :memory: as the path parameter.

Supported options fields include:

  • timeout: busy timeout in milliseconds
  • defaultQueryTimeout: default maximum query execution time in milliseconds before interruption

Per-query timeout override is available via queryOptions, for example:

  • db.exec("SELECT 1", { queryTimeout: 100 })
  • stmt.get(undefined, { queryTimeout: 100 })

The function returns a Database object.

prepare(sql) ⇒ Statement

Prepares a SQL statement for execution.

Param Type Description
sql string The SQL statement string to prepare.

The function returns a Statement object.

run(sql[, ...bindParameters][, queryOptions]) ⇒ object

Convenience wrapper that prepares sql and executes Statement.run. Returns the same info object as Statement.run (changes and lastInsertRowid). The internally prepared statement is finalized before the call returns.

Param Type Description
sql string The SQL statement string.
bindParameters any Optional positional or named bind parameters.
queryOptions object Optional per-query overrides (for example, { queryTimeout: 100 }).

Note: This is an extension in libSQL and not available in better-sqlite3.

get(sql[, ...bindParameters][, queryOptions]) ⇒ row

Convenience wrapper that prepares sql and executes Statement.get. Returns the first row, or undefined if no row matched. The internally prepared statement is finalized before the call returns.

Param Type Description
sql string The SQL statement string.
bindParameters any Optional positional or named bind parameters.
queryOptions object Optional per-query overrides (for example, { queryTimeout: 100 }).

Note: This is an extension in libSQL and not available in better-sqlite3.

all(sql[, ...bindParameters][, queryOptions]) ⇒ array of rows

Convenience wrapper that prepares sql and executes Statement.all. Returns all matching rows as an array. The internally prepared statement is finalized before the call returns.

Param Type Description
sql string The SQL statement string.
bindParameters any Optional positional or named bind parameters.
queryOptions object Optional per-query overrides (for example, { queryTimeout: 100 }).

Note: This is an extension in libSQL and not available in better-sqlite3.

iterate(sql[, ...bindParameters][, queryOptions]) ⇒ async iterator

Convenience wrapper that prepares sql and executes Statement.iterate. Returns an async iterator over the resulting rows. The internally prepared statement is finalized when the iterator is exhausted or closed.

Param Type Description
sql string The SQL statement string.
bindParameters any Optional positional or named bind parameters.
queryOptions object Optional per-query overrides (for example, { queryTimeout: 100 }).

Note: This is an extension in libSQL and not available in better-sqlite3.

transaction(function) ⇒ function

Returns a function that runs the given function in a transaction.

Param Type Description
function function The function to run in a transaction.

pragma(string, [options]) ⇒ results

Executes the given PRAGMA and returns its results.

Param Type Description
source string Pragma to be executed
options object Options.

Most PRAGMA return a single value, the simple: boolean option is provided to return the first column of the first row.

js
db.pragma('cache_size = 32000');
console.log(db.pragma('cache_size', { simple: true })); // => 32000

backup(destination, [options]) ⇒ promise

This function is currently not supported.

serialize([options]) ⇒ Buffer

This function is currently not supported.

function(name, [options], function) ⇒ this

This function is currently not supported.

aggregate(name, options) ⇒ this

This function is currently not supported.

table(name, definition) ⇒ this

This function is currently not supported.

loadExtension(path, [entryPoint]) ⇒ this

Loads a SQLite3 extension.

Param Type Description
path string The path to the extension to be loaded.

exec(sql) ⇒ this

Executes a SQL statement.

Param Type Description
sql string The SQL statement string to execute.

This can execute strings that contain multiple SQL statements.

interrupt() ⇒ this

Cancel ongoing operations and make them return at earliest opportunity.

Note: This is an extension in libSQL and not available in better-sqlite3.

This function is currently not supported.

close() ⇒ this

Closes the database connection.

class Statement

Methods

run([...bindParameters]) ⇒ object

Executes the SQL statement and (currently) returns an array with results.

Note: It should return an info object.

Param Type Description
bindParameters array of objects The bind parameters for executing the statement.

The returned info object contains two properties: changes that describes the number of modified rows and info.lastInsertRowid that represents the rowid of the last inserted row.

This function is currently not supported.

get([...bindParameters]) ⇒ row

Executes the SQL statement and returns the first row.

Param Type Description
bindParameters array of objects The bind parameters for executing the statement.

all([...bindParameters]) ⇒ array of rows

Executes the SQL statement and returns an array of the resulting rows.

Param Type Description
bindParameters array of objects The bind parameters for executing the statement.

iterate([...bindParameters]) ⇒ iterator

Executes the SQL statement and returns an iterator to the resulting rows.

Param Type Description
bindParameters array of objects The bind parameters for executing the statement.

pluck([toggleState]) ⇒ this

Makes the prepared statement only return the value of the first column of any rows that it retrieves.

Param Type Description
pluckMode boolean Enable of disable pluck mode. If you don't pass the paramenter, pluck mode is enabled.
js
stmt.pluck(); // plucking ON
stmt.pluck(true); // plucking ON
stmt.pluck(false); // plucking OFF

NOTE: When plucking is turned on, raw mode is turned off (they are mutually exclusive options).

expand([toggleState]) ⇒ this

This function is currently not supported.

raw([rawMode]) ⇒ this

Makes the prepared statement return rows as arrays instead of objects.

Param Type Description
rawMode boolean Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled.

This function enables or disables raw mode. Prepared statements return objects by default, but if raw mode is enabled, the functions return arrays instead.

js
stmt.raw(); // raw mode ON
stmt.raw(true); // raw mode ON
stmt.raw(false); // raw mode OFF

NOTE: When raw mode is turned on, plucking is turned off (they are mutually exclusive options).

columns() ⇒ array of objects

Returns the columns in the result set returned by this prepared statement.

This function is currently not supported.

bind([...bindParameters]) ⇒ this

Param Type Description
bindParameters array of objects The bind parameters for executing the statement.

Binds permanently the given parameters to the statement. After a statement's parameters are bound this way, you may no longer provide it with execution-specific (temporary) bound parameters.


File: bindings/javascript/packages/wasm-common/README.md

About

This package is the Turso embedded database common JS library which is shared between final builds for Node and Browser.

Do not use this package directly - instead you must use @tursodatabase/database or @tursodatabase/database-wasm.

Turso Database runs in production today at multiple organizations. It has not yet reached 1.0, so as with any database we recommend keeping backups — see the FAQ for where the project stands.


File: bindings/javascript/packages/wasm/README.md


About

This package is the Turso embedded database library for JavaScript in Browser.

Turso Database runs in production today at multiple organizations. It has not yet reached 1.0, so as with any database we recommend keeping backups — see the FAQ for where the project stands.

Features

  • SQLite compatible: SQLite query language and file format support (status).
  • In-process: No network overhead, runs directly in your Node.js process
  • TypeScript support: Full TypeScript definitions included

Installation

bash
npm install @tursodatabase/database-wasm

Getting Started

In-Memory Database

javascript
import { connect } from '@tursodatabase/database-wasm';

// Create an in-memory database
const db = await connect(':memory:');

// Create a table
await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');

// Insert data
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
await insert.run('Alice', '[email protected]');
await insert.run('Bob', '[email protected]');

// Query data
const users = await db.prepare('SELECT * FROM users').all();
console.log(users);
// Output: [
//   { id: 1, name: 'Alice', email: '[email protected]' },
//   { id: 2, name: 'Bob', email: '[email protected]' }
// ]

File-Based Database

javascript
import { connect } from '@tursodatabase/database-wasm';

// Create or open a database file
const db = await connect('my-database.db');

// Create a table
await db.exec(`
  CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);

// Insert a post
const insertPost = db.prepare('INSERT INTO posts (title, content) VALUES (?, ?)');
const result = await insertPost.run('Hello World', 'This is my first blog post!');

console.log(`Inserted post with ID: ${result.lastInsertRowid}`);

Transactions

javascript
import { connect } from '@tursodatabase/database-wasm';

const db = await connect('transactions.db');

// Using transactions for atomic operations
const transaction = db.transactionAsync(async (txn, users) => {
  const insert = await txn.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
  for (const user of users) {
    await insert.run(user.name, user.email);
  }
});

// Execute transaction
await transaction([
  { name: 'Alice', email: '[email protected]' },
  { name: 'Bob', email: '[email protected]' }
]);

API Reference

For complete API documentation, see JavaScript API Reference.

License

This project is licensed under the MIT license.

Support


File: bindings/javascript/packages/native/README.md


About

This package is the Turso embedded database library for JavaScript in Node.

Turso Database runs in production today at multiple organizations. It has not yet reached 1.0, so as with any database we recommend keeping backups — see the FAQ for where the project stands.

Features

  • SQLite compatible: SQLite query language and file format support (status).
  • In-process: No network overhead, runs directly in your Node.js process
  • TypeScript support: Full TypeScript definitions included
  • Cross-platform: Supports Linux (x86 and arm64), macOS, Windows (browser is supported in the separate package @tursodatabase/database-wasm package)

Installation

bash
npm install @tursodatabase/database

Getting Started

In-Memory Database

javascript
import { connect } from '@tursodatabase/database';

// Create an in-memory database
const db = await connect(':memory:');

// Create a table
await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');

// Insert data
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
await insert.run('Alice', '[email protected]');
await insert.run('Bob', '[email protected]');

// Query data
const users = await db.prepare('SELECT * FROM users').all();
console.log(users);
// Output: [
//   { id: 1, name: 'Alice', email: '[email protected]' },
//   { id: 2, name: 'Bob', email: '[email protected]' }
// ]

File-Based Database

javascript
import { connect } from '@tursodatabase/database';

// Create or open a database file
const db = await connect('my-database.db');

// Create a table
await db.exec(`
  CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);

// Insert a post
const insertPost = db.prepare('INSERT INTO posts (title, content) VALUES (?, ?)');
const result = await insertPost.run('Hello World', 'This is my first blog post!');

console.log(`Inserted post with ID: ${result.lastInsertRowid}`);

Transactions

javascript
import { connect } from '@tursodatabase/database';

const db = await connect('transactions.db');

// Using transactions for atomic operations
const transaction = db.transactionAsync(async (txn, users) => {
  const insert = await txn.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
  for (const user of users) {
    await insert.run(user.name, user.email);
  }
});

// Execute transaction
await transaction([
  { name: 'Alice', email: '[email protected]' },
  { name: 'Bob', email: '[email protected]' }
]);

API Reference

For complete API documentation, see JavaScript API Reference.

License

This project is licensed under the MIT license

Support


File: bindings/javascript/packages/common/README.md

About

This package is the Turso embedded database common JS library which is shared between final builds for Node and Browser.

Do not use this package directly - instead you must use @tursodatabase/database or @tursodatabase/database-wasm.

Turso Database runs in production today at multiple organizations. It has not yet reached 1.0, so as with any database we recommend keeping backups — see the FAQ for where the project stands.


File: bindings/javascript/sync/packages/wasm/README.md


About

This package is the Turso embedded database library for JavaScript in Browser.

Turso Database runs in production today at multiple organizations. It has not yet reached 1.0, so as with any database we recommend keeping backups — see the FAQ for where the project stands.

Features

  • SQLite compatible: SQLite query language and file format support (status).
  • In-process: No network overhead, runs directly in your Node.js process
  • TypeScript support: Full TypeScript definitions included

Installation

bash
npm install @tursodatabase/database-wasm

Getting Started

In-Memory Database

javascript
import { connect } from '@tursodatabase/database-wasm';

// Create an in-memory database
const db = await connect(':memory:');

// Create a table
await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');

// Insert data
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
await insert.run('Alice', '[email protected]');
await insert.run('Bob', '[email protected]');

// Query data
const users = await db.prepare('SELECT * FROM users').all();
console.log(users);
// Output: [
//   { id: 1, name: 'Alice', email: '[email protected]' },
//   { id: 2, name: 'Bob', email: '[email protected]' }
// ]

File-Based Database

javascript
import { connect } from '@tursodatabase/database-wasm';

// Create or open a database file
const db = await connect('my-database.db');

// Create a table
await db.exec(`
  CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);

// Insert a post
const insertPost = db.prepare('INSERT INTO posts (title, content) VALUES (?, ?)');
const result = await insertPost.run('Hello World', 'This is my first blog post!');

console.log(`Inserted post with ID: ${result.lastInsertRowid}`);

Transactions

javascript
import { connect } from '@tursodatabase/database-wasm';

const db = await connect('transactions.db');

// Using transactions for atomic operations
const transaction = db.transactionAsync(async (txn, users) => {
  const insert = await txn.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
  for (const user of users) {
    await insert.run(user.name, user.email);
  }
});

// Execute transaction
await transaction([
  { name: 'Alice', email: '[email protected]' },
  { name: 'Bob', email: '[email protected]' }
]);

API Reference

For complete API documentation, see JavaScript API Reference.

License

This project is licensed under the MIT license.

Support


File: bindings/javascript/sync/packages/native/README.md


About

This package is the Turso embedded database library for JavaScript in Node.

Turso Database runs in production today at multiple organizations. It has not yet reached 1.0, so as with any database we recommend keeping backups — see the FAQ for where the project stands.

Features

  • SQLite compatible: SQLite query language and file format support (status).
  • In-process: No network overhead, runs directly in your Node.js process
  • TypeScript support: Full TypeScript definitions included
  • Cross-platform: Supports Linux (x86 and arm64), macOS, Windows (browser is supported in the separate package @tursodatabase/database-wasm package)

Installation

bash
npm install @tursodatabase/database

Getting Started

In-Memory Database

javascript
import { connect } from '@tursodatabase/database';

// Create an in-memory database
const db = await connect(':memory:');

// Create a table
await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');

// Insert data
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
await insert.run('Alice', '[email protected]');
await insert.run('Bob', '[email protected]');

// Query data
const users = await db.prepare('SELECT * FROM users').all();
console.log(users);
// Output: [
//   { id: 1, name: 'Alice', email: '[email protected]' },
//   { id: 2, name: 'Bob', email: '[email protected]' }
// ]

File-Based Database

javascript
import { connect } from '@tursodatabase/database';

// Create or open a database file
const db = await connect('my-database.db');

// Create a table
await db.exec(`
  CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);

// Insert a post
const insertPost = db.prepare('INSERT INTO posts (title, content) VALUES (?, ?)');
const result = await insertPost.run('Hello World', 'This is my first blog post!');

console.log(`Inserted post with ID: ${result.lastInsertRowid}`);

Transactions

javascript
import { connect } from '@tursodatabase/database';

const db = await connect('transactions.db');

// Using transactions for atomic operations
const transaction = db.transactionAsync(async (txn, users) => {
  const insert = await txn.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
  for (const user of users) {
    await insert.run(user.name, user.email);
  }
});

// Execute transaction
await transaction([
  { name: 'Alice', email: '[email protected]' },
  { name: 'Bob', email: '[email protected]' }
]);

API Reference

For complete API documentation, see JavaScript API Reference.

License

This project is licensed under the MIT license.

Support


File: bindings/javascript/sync/packages/common/README.md

About

This package is the Turso Sync common JS library which is shared between final builds for Node and Browser.

Do not use this package directly - instead you must use @tursodatabase/sync or @tursodatabase/sync-wasm.

Turso Database runs in production today at multiple organizations. It has not yet reached 1.0, so as with any database we recommend keeping backups — see the FAQ for where the project stands.


File: bindings/javascript/sync/README.md


About

This package is for syncing local Turso databases to the Turso Cloud and back.

Turso Database runs in production today at multiple organizations. It has not yet reached 1.0, so as with any database we recommend keeping backups — see the FAQ for where the project stands.

Installation

bash
npm install @tursodatabase/sync

Getting Started

To sync a database hosted at Turso Cloud:

js
import { connect } from '@tursodatabase/sync';

const db = await connect({
    path: 'local.db',                // path used as a prefix for local files created by sync-engine
    url: 'https://<db>.turso.io',    // URL of the remote database: turso db show <db>
    authToken: '...',                // auth token issued from the Turso Cloud: turso db tokens create <db>
    clientName: 'turso-sync-example' // arbitrary client name
});

// db has same functions as Database class from @tursodatabase/database package but adds few more methods for sync:
await db.pull(); // pull changes from the remote
await db.push(); // push changes to the remote
await db.sync(); // pull & push changes

License

This project is licensed under the MIT license.

Support

2. Official Technical Reference & Guides (tursodatabase/turso-docs)

File: agentfs/guides/auditing.mdx


title: Audit Filesystem Changes sidebarTitle: Audit Filesystem Changes

AgentFS records every file operation and tool call, giving you complete visibility into agent behavior. This is essential for debugging, compliance, and understanding how agents work.

Viewing the Timeline

See a chronological list of tool calls:

bash
agentfs timeline my-session

Output:

text
ID   TOOL                 STATUS       DURATION STARTED
4    execute_code         pending            -- 2024-01-05 09:44:20
3    api_call             error           300ms 2024-01-05 09:44:15
2    read_file            success          50ms 2024-01-05 09:44:10
1    web_search           success        1200ms 2024-01-05 09:43:45

Filtering Results

Show only specific tool types:

bash
agentfs timeline my-session --filter web_search

Show only errors:

bash
agentfs timeline my-session --status error

Limit number of entries:

bash
agentfs timeline my-session --limit 20

JSON Output

For programmatic analysis:

bash
agentfs timeline my-session --format json
json
[
  {
    "id": 1,
    "name": "web_search",
    "status": "success",
    "started_at": 1704447825,
    "completed_at": 1704447826,
    "duration_ms": 1200,
    "parameters": {"query": "AI agents"},
    "result": {"results": ["result1", "result2"]}
  }
]

Inspecting Files

List all files in a session:

bash
agentfs fs ls my-session

Output:

text
d artifacts
f config.json
f output.txt

List a subdirectory:

bash
agentfs fs ls my-session /artifacts

Read file contents:

bash
agentfs fs cat my-session /output.txt

Viewing Changes (Diff)

See what changed compared to the original filesystem:

bash
agentfs diff my-session

This shows:

  • New files created
  • Modified files
  • Deleted files

Querying with SQL

Since AgentFS uses SQLite, you can run arbitrary queries:

```bash tursodb .agentfs/my-session.db ``` ```bash sqlite3 .agentfs/my-session.db ```

Example queries:

sql
-- Find all tool calls that took longer than 1 second
SELECT name, duration_ms
FROM toolcalls
WHERE duration_ms > 1000;

-- Count tool usage by type
SELECT name, COUNT(*) as count
FROM toolcalls
GROUP BY name
ORDER BY count DESC;

-- Find files modified in the last hour
SELECT path, mtime
FROM fs_inode
WHERE mtime > strftime('%s', 'now', '-1 hour');

-- Get total bytes written
SELECT SUM(size) as total_bytes
FROM fs_inode
WHERE mode & 0170000 = 0100000;  -- regular files only

Use Cases

Debugging Agent Failures

When an agent fails, use the timeline to understand what happened:

bash
# See recent activity
agentfs timeline my-session --limit 20

# Focus on errors
agentfs timeline my-session --status error

# Check what files were created/modified
agentfs fs ls my-session

Performance Analysis

Find slow operations:

sql
SELECT name, AVG(duration_ms) as avg_ms, COUNT(*) as count
FROM toolcalls
GROUP BY name
ORDER BY avg_ms DESC;

Compliance Auditing

Export a complete record of agent activity:

bash
agentfs timeline my-session --format json > audit-log.json

Next Steps

Understanding copy-on-write Sync sessions to Turso Cloud

File: agentfs/guides/mcp.mdx


title: MCP Server Integration sidebarTitle: MCP Server Integration

AgentFS can run as an MCP (Model Context Protocol) server, exposing filesystem and key-value operations to AI assistants like Claude Desktop.

Starting the MCP Server

bash
agentfs serve mcp my-agent

This starts an MCP server that exposes AgentFS tools over stdio.

Available Tools

Filesystem Tools

Tool Description
read_file Read file contents
write_file Write file contents
readdir List directory contents
mkdir Create directory
remove Remove file or directory
rename Rename file or directory
stat Get file metadata
access Check file access permissions

Key-Value Tools

Tool Description
kv_get Get value by key
kv_set Set key-value pair
kv_delete Delete key
kv_list List all keys

Limiting Exposed Tools

For security, you can limit which tools are available:

bash
# Read-only mode
agentfs serve mcp my-agent --tools read_file,readdir,stat,kv_get,kv_list

# Filesystem only
agentfs serve mcp my-agent --tools read_file,write_file,readdir,mkdir,remove

# Key-value only
agentfs serve mcp my-agent --tools kv_get,kv_set,kv_delete,kv_list

Security Considerations

The MCP server gives AI assistants direct access to the AgentFS filesystem. Consider these security practices:
  • Limit tools - Only expose tools the assistant needs
  • Use overlays - Create overlay filesystems instead of giving access to real directories
  • Review the agent database - Check .agentfs/*.db periodically

Debugging

Check if the MCP server is working:

bash
# Run manually to see output
agentfs serve mcp my-agent

# Check if the database exists
ls -la .agentfs/my-agent.db

Next Steps

Remote access via NFS How isolation works

File: agentfs/guides/nfs.mdx


title: NFS Server Access sidebarTitle: NFS Server Access

AgentFS can expose filesystems over NFS, enabling remote access from other machines, containers, or virtual machines.

Starting the NFS Server

bash
agentfs serve nfs my-agent

By default, this listens on 127.0.0.1:11111.

Binding to All Interfaces

To allow remote access:

bash
agentfs serve nfs my-agent --bind 0.0.0.0 --port 2049
Exposing NFS on `0.0.0.0` makes the filesystem accessible to anyone who can reach your machine. Use firewall rules or a VPN for production deployments.

Mounting the Filesystem

From the Same Machine

bash
mkdir /mnt/agentfs
mount -t nfs -o vers=3,tcp,port=11111,mountport=11111,nolock 127.0.0.1:/ /mnt/agentfs

From a Remote Machine

bash
mount -t nfs -o vers=3,tcp,port=2049,mountport=2049,nolock server-ip:/ /mnt/agentfs

Mount Options Explained

Option Description
vers=3 Use NFSv3 protocol
tcp Use TCP transport
port=11111 NFS server port
mountport=11111 Mount protocol port
nolock Disable file locking (single-user)

Unmounting

bash
umount /mnt/agentfs

If the mount is busy:

bash
umount -f /mnt/agentfs  # Force unmount
# or
umount -l /mnt/agentfs  # Lazy unmount

Next Steps

Expose AgentFS via MCP Back up and share agent state

File: agentfs/guides/overlay.mdx


title: Copy-on-Write Overlays sidebarTitle: Copy-on-Write Overlays

AgentFS uses an overlay filesystem to provide copy-on-write isolation. This lets agents freely modify files while keeping your original data safe.

How Overlay Works

An overlay filesystem combines two layers:

text
┌────────────────────────────────────┐
│         Merged View (what you see) │
├────────────────────────────────────┤
│  Delta Layer (AgentFS database)    │  ← Writes go here
├────────────────────────────────────┤
│  Base Layer (original directory)   │  ← Read-only
└────────────────────────────────────┘

Reading a file:

  1. Check delta layer first
  2. If not found, read from base layer

Writing a file:

  1. Copy file to delta layer (if from base)
  2. Write changes to delta layer
  3. Base layer is never modified

Deleting a file:

  1. Create a "whiteout" marker in delta layer
  2. File appears deleted in merged view
  3. Original file in base layer is untouched

Creating an Overlay Filesystem

With `agentfs run`

The simplest way - automatically creates an overlay over your current directory:

bash
cd /path/to/project
agentfs run /bin/bash

With `agentfs init --base`

Create an overlay explicitly:

bash
agentfs init my-overlay --base /path/to/project

Then mount it:

bash
agentfs mount my-overlay ./workspace

Or run a command in it:

bash
agentfs run --session my-overlay /bin/bash

Viewing Changes

See what's different from the base:

bash
agentfs diff my-overlay

Practical Examples

Safe Refactoring

Let an agent refactor your code without risk:

bash
cd my-project
agentfs run --session refactor python3 refactor_agent.py

# Review what changed
agentfs diff refactor

# Happy? Apply changes manually or via script
# Not happy? Just delete the session
rm .agentfs/refactor.db

Testing Destructive Operations

Try something dangerous safely:

bash
agentfs run --session test /bin/bash
$ rm -rf src/  # Yikes!
$ exit

# Original files are fine
ls src/  # Still there!

# Only the overlay was affected
agentfs fs ls test  # Shows the deletion

Parallel Experiments

Run multiple experiments on the same codebase:

bash
# Experiment A
agentfs run --session exp-a python3 approach_a.py

# Experiment B (same base, different changes)
agentfs run --session exp-b python3 approach_b.py

# Compare results
agentfs diff exp-a
agentfs diff exp-b

Storage Efficiency

The overlay only stores:

  • Modified files - Full copy after first write
  • New files - Stored entirely in delta
  • Deleted files - Small whiteout marker

Original files are never duplicated. A 10GB project with 1MB of changes only uses ~1MB in the delta layer.

Technical Details

File Operations

Operation Behavior
Read existing file Pass through to base
Write existing file Copy to delta, then write
Create new file Write directly to delta
Delete file Create whiteout in delta
Rename file Delete old + create new

Whiteouts

When you delete a base file, AgentFS creates a "whiteout" - a marker that hides the file:

sql
-- Whiteouts in the database
SELECT * FROM fs_inode WHERE whiteout = 1;

Database Schema

The delta layer is stored in SQLite:

sql
-- Files and directories
CREATE TABLE fs_inode (
    ino INTEGER PRIMARY KEY,
    parent_ino INTEGER,
    name TEXT,
    mode INTEGER,
    size INTEGER,
    ...
    whiteout INTEGER DEFAULT 0
);

-- File contents
CREATE TABLE fs_block (
    ino INTEGER,
    block_num INTEGER,
    data BLOB
);

See the AgentFS Specification for the complete schema.

Next Steps

Share state between agents Sync overlays to Turso Cloud

File: agentfs/guides/sandbox.mdx


title: Agentic Coding sidebarTitle: Agentic Coding

AgentFS lets you run AI coding agents like Claude Code, Codex, and OpenCode in a sandboxed environment where file changes are isolated from your original source tree.

Supported Agents

Agent Command
Claude Code agentfs run claude
Codex agentfs run codex
OpenCode agentfs run opencode
Any CLI tool agentfs run <command>

Workflow

Here's a typical agentic coding workflow. This example uses Claude Code, but the workflow is the same with OpenCode, Codex, and others.

Step 1: Clone the Repository

Clone the git repository to a host directory. Keep this source tree clean from build artifacts. When there's a merge to main, pull to this repository to keep it up-to-date.

bash
git clone https://github.com/tursodatabase/agentfs.git
cd agentfs

Step 2: Start an AgentFS Session

Use agentfs run with the --session flag to start a session. Pick a name that can eventually become your git branch name.

```bash agentfs run --session fix-bug-123 claude ``` ```bash agentfs run --session fix-bug-123 codex ``` ```bash agentfs run --session fix-bug-123 opencode ```

The agent now runs in a copy-on-write environment. Your original source tree remains untouched while the agent works.

Step 3 (Optional): Manual Investigation

If the agent does something unusual, open another terminal and join the same session to investigate:

bash
agentfs run --session fix-bug-123 bash

The filesystem is shared between the shell and the agent. You can browse files, run tests, or check what's happening while the agent works.

Step 4: Push to Branch

Once the agent has completed the work, ask it to create a branch, commit the code, and push:

"Create a branch called fix-bug-123, commit these changes, and push to origin"

You can then create a PR manually from the pushed branch.

Why This Workflow?

  • Clean source tree - Your original clone stays pristine and easy to update from upstream
  • Isolated changes - Each task gets its own session, preventing cross-contamination
  • Easy investigation - Join any session to see what's happening in real-time
  • Standard git flow - Push branches and create PRs as usual

Next Steps

Share state between agents and terminals Inspect what an agent did with diff and timeline

File: agentfs/guides/sessions.mdx


title: Shared Agent Sessions sidebarTitle: Shared Agent Sessions

Named sessions let you share a filesystem between multiple agents, terminals, or runs. All participants see the same copy-on-write view of your project.

Creating a Session

Use --session to create a named session:

bash
agentfs run --session my-project /bin/bash

This creates a session called my-project. All changes are stored in .agentfs/my-project.db.

Resuming a Session

Run the same command to resume where you left off:

bash
# First run - make some changes
agentfs run --session my-project /bin/bash
$ echo "hello" > test.txt
$ exit

# Second run - changes are still there
agentfs run --session my-project /bin/bash
$ cat test.txt
hello

Multi-Terminal Collaboration

Multiple terminals can share the same session simultaneously:

bash
# Terminal 1
agentfs run --session shared-work /bin/bash

# Terminal 2 - joins the same filesystem
agentfs run --session shared-work python3 agent.py

# Terminal 3 - also sees the same files
agentfs run --session shared-work vim

All terminals share the same copy-on-write filesystem. Changes made in one terminal are immediately visible in others.

Use Cases

Iterative Development

Work on a task across multiple sessions:

bash
# Morning session
agentfs run --session feature-x /bin/bash
# ... work on the feature, then exit for lunch

# Afternoon session - continue where you left off
agentfs run --session feature-x /bin/bash

Safe Experimentation

Create a session, try something risky, then decide whether to keep it:

bash
agentfs run --session experiment /bin/bash
# ... try some changes

# Option A: Happy with changes
agentfs diff experiment  # Review
# Apply changes to real filesystem (manually or via script)

# Option B: Discard everything
rm .agentfs/experiment.db

Agent Supervision

Run an agent and monitor its work from another terminal:

bash
# Terminal 1: Run the agent
agentfs run --session agent-task python3 agent.py

# Terminal 2: Watch what it's doing
agentfs run --session agent-task /bin/bash
$ watch ls -la  # See files being created
$ tail -f agent.log  # Monitor logs

Managing Sessions

List files in a session:

bash
agentfs fs ls my-session

View session changes:

bash
agentfs diff my-session

Delete a session:

bash
rm .agentfs/my-session.db

Session Storage

Sessions are stored as SQLite databases in .agentfs/:

text
.agentfs/
├── my-project.db
├── experiment.db
└── shared-work.db

Each database contains:

  • All modified files
  • Deleted file markers (whiteouts)
  • Tool call audit log
  • Key-value store data

Next Steps

Inspect what happened in a session How copy-on-write works

File: agentfs/guides/sync.mdx


title: Sync with Turso Cloud sidebarTitle: Sync with Turso Cloud

AgentFS can sync with Turso Cloud, enabling backup, collaboration, and distributed agent deployments.

Setting Up Sync

1. Create a Turso Database

If you don't have a Turso account, sign up first.

Create a database for your agent:

bash
turso db create my-agent-db

Get the database URL:

bash
turso db show my-agent-db --url
# Output: libsql://my-agent-db-username.turso.io

2. Initialize with Sync

Create a new agent filesystem with sync enabled:

bash
agentfs init my-agent --sync-remote-url libsql://my-agent-db-username.turso.io

Or add sync to an existing agent:

bash
# Edit the database to add sync configuration
# (Feature coming soon)

Sync Commands

Pull Remote Changes

Download changes from Turso Cloud:

bash
agentfs sync my-agent pull

Push Local Changes

Upload local changes to Turso Cloud:

bash
agentfs sync my-agent push

View Sync Status

Check sync statistics:

bash
agentfs sync my-agent stats

Create Checkpoint

Force a sync checkpoint:

bash
agentfs sync my-agent checkpoint

Use Cases

Backup and Recovery

Automatically back up agent state:

bash
# After important work
agentfs sync my-agent push

# On a new machine
agentfs init my-agent --sync-remote-url libsql://...
agentfs sync my-agent pull

Multi-Machine Agents

Run agents on multiple machines with shared state:

bash
# Machine A
agentfs run --session shared-agent python3 agent.py
agentfs sync shared-agent push

# Machine B
agentfs sync shared-agent pull
agentfs run --session shared-agent python3 agent.py

Partial Sync

For large filesystems, use partial sync to only fetch what's needed:

bash
agentfs init my-agent \
  --sync-remote-url libsql://... \
  --sync-partial-prefetch \
  --sync-partial-segment-size 1000

Partial Sync Options

Option Description
--sync-partial-prefetch Enable prefetching
--sync-partial-segment-size Segment size for partial sync
--sync-partial-bootstrap-query Custom bootstrap query
--sync-partial-bootstrap-length Bootstrap prefix length

Authentication

Sync requires a Turso auth token. Set it via environment variable:

bash
export TURSO_AUTH_TOKEN=$(turso db tokens create my-agent-db)

Or create a long-lived token:

bash
turso db tokens create my-agent-db --expiration none

Conflict Resolution

AgentFS uses last-write-wins for conflicts. If the same file is modified on multiple machines:

  1. Push from Machine A succeeds
  2. Push from Machine B fails (conflict)
  3. Machine B must pull first, then push

Next Steps

Learn more about Turso Cloud Remote access via NFS

File: guides/agent-databases.mdx


title: Agent Databases

Looking for a complete agent state management solution? Check out AgentFS - our specialized SDK designed specifically for AI agents, offering filesystem operations, key-value storage, and automatic tool call tracking in a single database file.

AI agents need databases to store context, track state, and maintain memory across executions. Turso Database offers two powerful approaches:

  • Embedded Databases for local-first agent processing, and

  • Turso Sync for distributed agent coordination with cloud persistence.

Embedded Databases (Local-First)

Use embedded databases for agents that process data locally within a single workflow, need offline-capable access, or work with temporary data without sharing state with other agents.

Local data pipelines, on-device processing of sensitive data, development/testing workflows, and single-agent analysis tasks.

javascript
import { connect } from "@tursodatabase/database";

// Create a local embedded database for this agent
const db = await connect("agent-workflow.db");

// Agent can now store and query data locally
const stmt = db.prepare(`
  CREATE TABLE IF NOT EXISTS steps (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    action TEXT NOT NULL,
    result TEXT,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);
stmt.run();

// Track agent workflow steps
db.prepare("INSERT INTO steps (action, result) VALUES (?, ?)").run(
  "fetch_data",
  "success",
);

Benefit from zero network latency, full offline capability, complete data privacy, and simple single-file deployment.

Data is isolated to this agent instance with no built-in multi-agent collaboration or cross-session persistence (unless you manage the file yourself).

Turso Sync (Cloud-Connected)

Use Turso Sync for agents that need persistent memory across sessions, coordinate with other agents, or require cloud-backed durability for long-running workflows.

💡 Conversational agents with session memory, multi-agent coordination systems, long-running workflows with recovery needs, and agents reporting to central systems.

javascript
import { connect } from "@tursodatabase/database";

// Connect to a synced database (embedded locally, synced to Turso Cloud)
const db = await connect({
  path: "agent-memory.db",
  url: "https://your-database.turso.io",
  authToken: "your-auth-token",
  sync: "full", // Bidirectional sync
});

// Agent can access data from previous sessions or other agents
const previousContext = db
  .prepare("SELECT * FROM conversation_history WHERE session_id = ?")
  .all("session-123");

// New data is automatically synced to cloud and other agent instances
db.prepare("INSERT INTO agent_actions (agent_id, action) VALUES (?, ?)").run(
  "agent-1",
  "processed_document",
);

// Manually trigger sync if needed
await db.sync();

💡 Persistent memory across restarts, multi-agent coordination via shared state, cloud backup and durability, with fast local reads and background sync.

Sync modes: sync (bidirectional), pull (cloud → agent), push (agent → cloud).

Multi-Agent Architectures

Pattern 1: Isolated Agent Databases

Each agent gets its own embedded database. Best for independent agents with no shared state.

javascript
// Agent 1
const agent1DB = await connect("agent-1.db");

// Agent 2
const agent2DB = await connect("agent-2.db");

Independent agents performing separate tasks with no coordination needs and maximum isolation.

Pattern 2: Shared Database with Sync

Multiple agents connect to the same synced database. Best for coordinated agent systems.

javascript
// Agent 1 connects and writes tasks
const agent1DB = await connect({
  path: "agent-1-local.db",
  url: "https://shared-tasks.turso.io",
  authToken: process.env.TURSO_TOKEN,
  sync: "full",
});

agent1DB.prepare("INSERT INTO tasks (status) VALUES (?)").run("pending");

// Agent 2 connects to same database and reads tasks
const agent2DB = await connect({
  path: "agent-2-local.db",
  url: "https://shared-tasks.turso.io", // Same database
  authToken: process.env.TURSO_TOKEN,
  sync: "full",
});

const pendingTasks = agent2DB
  .prepare("SELECT * FROM tasks WHERE status = ?")
  .all("pending");

💡 Agents coordinating work through shared task queues or a common knowledge base.

Pattern 3: Hub-and-Spoke

A central synced database with agents pushing results and pulling new tasks.

javascript
// Worker agents push results only
const workerDB = await connect({
  path: "worker-local.db",
  url: "https://hub.turso.io",
  authToken: process.env.WORKER_TOKEN,
  sync: "push", // Only push results to cloud
});

// Coordinator agent pulls from all workers
const coordinatorDB = await connect({
  path: "coordinator-local.db",
  url: "https://hub.turso.io",
  authToken: process.env.COORDINATOR_TOKEN,
  sync: "pull", // Only pull data from cloud
});

💡 Many worker agents feeding results to a central coordinator for monitoring or aggregation with unidirectional data flow.

Database-Per-Agent Pattern

For systems managing many agents, you can create individual databases for each agent using the Platform API:

javascript
import { createClient } from "@tursodatabase/api";

const turso = createClient({
  token: process.env.TURSO_PLATFORM_API_TOKEN,
  org: process.env.TURSO_ORG_NAME,
});

// Create a dedicated database for each agent
async function provisionAgentDatabase(agentId) {
  const database = await turso.databases.create(`agent-${agentId}`, {
    group: "default",
  });

  console.log(`Database created for agent ${agentId}: ${database.Name}`);
  return database;
}

Complete isolation between agents, independent scaling per agent, improved security (compromised agent only affects one database), and easy cleanup when agents are retired.

Examples

Explore these examples to see agent database patterns in action:


File: guides/ai-memory.mdx


title: AI Memory

AI agents start every session from scratch. A persistent memory system lets agents store lessons and retrieve relevant context via vector search. Turso's embedded database with built-in vector functions makes this possible in a single local file.

This guide covers the schema and queries for building a memory system with vector similarity search.

Schema

At its simplest, a memory system needs just two tables: one for memories and one for tracking which memories were used in which tasks.

sql
CREATE TABLE IF NOT EXISTS memories (
    id              TEXT PRIMARY KEY,
    content         TEXT NOT NULL,
    embedding       F8_BLOB(384),
    category        TEXT NOT NULL,
    created_at      INTEGER NOT NULL,
    last_retrieved  INTEGER,
    retrieval_count INTEGER DEFAULT 0,
    source_task     TEXT
);

CREATE TABLE IF NOT EXISTS tasks (
    id               TEXT PRIMARY KEY,
    description      TEXT,
    embedding        F8_BLOB(384),
    started_at       INTEGER,
    finished_at      INTEGER
);
  • memories stores each lesson with its vector embedding and a category (e.g. correction, insight, user, discovery). The embedding is a 384-dimensional int8-quantized vector (F8_BLOB(384)).
  • tasks records each task the agent worked on, linking task descriptions to their embeddings for retrieval.

Connecting

javascript
import { connect } from "@tursodatabase/database";

const db = await connect(".memory/agent.db");
await db.exec(SCHEMA);

Storing a memory

When the agent learns something — a correction, a user preference, an insight — store it with an embedding. Use vector8() to convert a JSON float array into an int8-quantized vector:

sql
INSERT INTO memories (id, content, embedding, category, created_at, source_task)
VALUES (?, ?, vector8(?), ?, ?, ?);

The parameter for vector8() is a JSON-stringified float array (e.g. 384 dimensions from a model like all-MiniLM-L6-v2). Turso handles the quantization internally:

javascript
db.prepare(`
  INSERT INTO memories (id, content, embedding, category, created_at)
  VALUES (?, ?, vector8(?), ?, ?)
`).run(id, content, JSON.stringify(Array.from(embedding)), "correction", Date.now());

Retrieving relevant memories

When a new task starts, find the most relevant memories using Turso's vector_distance_cos() function:

sql
SELECT id, content, category, created_at, retrieval_count,
       vector_distance_cos(embedding, vector8(?)) AS distance
FROM memories
WHERE embedding IS NOT NULL
ORDER BY distance ASC
LIMIT ?;

vector_distance_cos() returns cosine distance (0 = identical, 2 = opposite). Lower is better. To convert to similarity: 1.0 - distance.

After retrieval, update the memory's metadata:

sql
UPDATE memories SET last_retrieved = ?, retrieval_count = retrieval_count + 1
WHERE id = ?;

Correcting wrong memories

When a memory turns out to be wrong, delete it and store a corrected version:

sql
DELETE FROM memories WHERE id = ?;

INSERT INTO memories (id, content, embedding, category, created_at)
VALUES (?, ?, vector8(?), 'correction', ?);

Task tracking

Record tasks to understand what the agent has been working on:

sql
-- Start a task
INSERT INTO tasks (id, description, embedding, started_at)
VALUES (?, ?, vector8(?), ?);

-- Complete a task
UPDATE tasks SET finished_at = ? WHERE id = ?;

Purging old memories

Remove memories that have been around a long time but are never retrieved:

sql
DELETE FROM memories
WHERE retrieval_count = 0 AND created_at < ?;

Key design points

  • Vector search uses Turso's vector8() and vector_distance_cos() for cosine similarity directly in SQL. Int8 quantization reduces storage by 75% compared to float32 with minimal impact on search quality.
  • For small per-project datasets, a full table scan with ORDER BY distance LIMIT k is fast enough without a vector index.
  • Everything runs in a single embedded database file with no external services.

Going further

The schema above is deliberately minimal. In practice you will want to experiment with strategies for keeping the memory store useful as it grows. Some ideas:

  • Weighted memories — add a weight REAL column and boost or penalize memories based on whether the agent found them useful after retrieval. Use an exponential moving average to update weights over time.
  • Time decay — multiply a recency factor into the retrieval ranking so that stale memories rank lower. For example: (1.0 - distance) * POWER(0.95, days_since_last_retrieved).
  • Garbage collection — periodically remove memories with low weight and high retrieval count (retrieved often, never useful), or memories that have never been retrieved after a threshold period.
  • Task scoring — track outcome metrics (tokens used, errors, corrections) per task and use them to compute a credit signal for the memories that were retrieved during that task.
  • Retrieval attribution — add a join table (memory_id, task_id, similarity, credit) to record exactly which memories were used in each task and how they were rated.

There is no single best approach — the right strategy depends on your agent's workload, how often memories are created, and how aggressively you want to prune. Start simple and iterate.

Example

memelord is an MCP server and hooks system that implements this pattern with reinforcement learning and weight decay as a persistent memory layer for AI coding agents.


File: guides/code-indexing.mdx


title: Code Indexing

AI coding agents work better when they can search a codebase semantically ("find the authentication logic") rather than just by filename or grep. A code index backed by Turso can combine full-text search over identifiers with vector similarity search over embeddings, all in a single embedded database.

This guide covers the schema and queries for building a code indexer with both FTS and vector search.

Schema

sql
CREATE TABLE IF NOT EXISTS codebases (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    root_path   TEXT NOT NULL UNIQUE,
    name        TEXT NOT NULL DEFAULT '',
    indexed_at  INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE IF NOT EXISTS chunks (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    codebase_id     INTEGER NOT NULL REFERENCES codebases(id),
    file_path       TEXT NOT NULL,
    chunk_key       TEXT NOT NULL UNIQUE,
    language        TEXT NOT NULL,
    kind            TEXT NOT NULL,
    name            TEXT NOT NULL DEFAULT '',
    signature       TEXT NOT NULL DEFAULT '',
    snippet         TEXT NOT NULL,
    start_line      INTEGER NOT NULL,
    end_line        INTEGER NOT NULL,
    file_hash       TEXT NOT NULL,
    indexed_at      INTEGER NOT NULL,
    embedding       F8_BLOB(384),
    embedding_model TEXT DEFAULT ''
);

CREATE TABLE IF NOT EXISTS indexed_files (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    codebase_id INTEGER NOT NULL REFERENCES codebases(id),
    file_path   TEXT NOT NULL,
    file_hash   TEXT NOT NULL,
    chunk_count INTEGER NOT NULL DEFAULT 0,
    indexed_at  INTEGER NOT NULL,
    UNIQUE(codebase_id, file_path)
);

How it fits together

  • codebases registers each project root. A single database can index multiple codebases.
  • chunks stores individual semantic units extracted from source files — functions, structs, classes, impl blocks, etc. Each chunk has a name, signature, code snippet, line range, and optionally a vector embedding. The chunk_key is a unique identifier (e.g. file_path::kind::name) for upsert operations.
  • indexed_files tracks which files have been indexed and their content hashes, enabling incremental re-indexing — only changed files are re-processed.

Connecting

The FTS index features require an experimental flag at connection time:

javascript
import { connect } from "@tursodatabase/database";

const db = await connect(".index/code.db", {
  experimental: ["index_method"],
});
await db.exec(SCHEMA);

The experimental: ["index_method"] flag enables Turso's USING fts index syntax and the fts_match() / fts_score() functions.

Indexing files

Upserting chunks

When a file is parsed, upsert its chunks. Setting embedding = NULL on conflict forces re-embedding when content changes:

sql
INSERT INTO chunks (codebase_id, file_path, chunk_key, language, kind, name,
                    signature, snippet, start_line, end_line, file_hash, indexed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(chunk_key) DO UPDATE SET
  language = excluded.language,
  kind = excluded.kind,
  name = excluded.name,
  signature = excluded.signature,
  snippet = excluded.snippet,
  start_line = excluded.start_line,
  end_line = excluded.end_line,
  file_hash = excluded.file_hash,
  indexed_at = excluded.indexed_at,
  embedding = NULL,
  embedding_model = '';

Tracking indexed files

sql
INSERT INTO indexed_files (codebase_id, file_path, file_hash, chunk_count, indexed_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(codebase_id, file_path) DO UPDATE SET
  file_hash = excluded.file_hash,
  chunk_count = excluded.chunk_count,
  indexed_at = excluded.indexed_at;

Skipping unchanged files

Before parsing a file, check if it has changed:

sql
SELECT file_hash FROM indexed_files WHERE codebase_id = ? AND file_path = ?;

If the hash matches, skip it entirely.

Storing embeddings

Embeddings can be stored as int8-quantized vectors using Turso's vector8() function, which reduces storage from 1,536 bytes (float32, 384 dims) to 395 bytes:

sql
UPDATE chunks SET embedding = vector8(?), embedding_model = ? WHERE chunk_key = ?;

The parameter for vector8() is a JSON-stringified float array. Turso handles the quantization internally.

Find chunks that need embedding:

sql
SELECT chunk_key, name, signature, file_path, kind, snippet
FROM chunks
WHERE codebase_id = ? AND (embedding IS NULL OR embedding_model != ?)
LIMIT ?;

Turso provides an FTS index with weighted BM25 scoring that is separate from SQLite's fts5 virtual tables.

Creating the FTS index

Create an FTS table per codebase and populate it from the chunks:

sql
-- Create the FTS table
CREATE TABLE IF NOT EXISTS fts_1 (
    chunk_id    INTEGER NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,
    name        TEXT NOT NULL DEFAULT '',
    signature   TEXT NOT NULL DEFAULT ''
);

-- Populate from chunks
INSERT INTO fts_1 (chunk_id, name, signature)
SELECT id, name, signature FROM chunks WHERE codebase_id = 1;

-- Create the FTS index with weighted columns
CREATE INDEX IF NOT EXISTS idx_fts_1 ON fts_1
    USING fts (name, signature)
    WITH (
        tokenizer = 'default',
        weights = 'name=5.0,signature=3.0'
    );

The weights parameter controls BM25 scoring — here, matches in the function/type name are weighted 5x and signature matches 3x.

Searching with FTS

sql
SELECT chunk_id, fts_score(name, signature, ?1) AS score
FROM fts_1
WHERE fts_match(name, signature, ?1)
ORDER BY score DESC
LIMIT ?;

Then fetch the full chunk data:

sql
SELECT chunk_key, file_path, name, kind, signature, snippet, start_line, end_line
FROM chunks WHERE id = ?;

For semantic/natural language queries, use vector cosine distance:

sql
SELECT chunk_key, file_path, name, kind, signature, snippet, start_line, end_line,
       vector_distance_cos(embedding, vector8(?)) AS distance
FROM chunks
WHERE embedding IS NOT NULL
ORDER BY distance ASC
LIMIT ?;

The score is 1 - distance (cosine similarity from cosine distance).

For the best results, combine both approaches using Reciprocal Rank Fusion (RRF). Run the FTS and vector queries separately, then merge results in application code:

javascript
// Run both searches
const ftsResults = await ftsSearch(query, limit);
const vecResults = await vectorSearch(queryEmbedding, limit);

// Reciprocal Rank Fusion
const k = 60; // RRF constant
const scores = new Map();

ftsResults.forEach((r, i) => {
  const key = r.chunk_key;
  scores.set(key, (scores.get(key) || 0) + 1 / (k + i + 1));
});

vecResults.forEach((r, i) => {
  const key = r.chunk_key;
  scores.set(key, (scores.get(key) || 0) + 1 / (k + i + 1));
});

// Sort by combined score
const merged = [...scores.entries()]
  .sort((a, b) => b[1] - a[1])
  .slice(0, limit);

Removing stale files

When files are deleted from the codebase, clean up their chunks and records:

sql
DELETE FROM chunks WHERE codebase_id = ? AND file_path = ?;
DELETE FROM indexed_files WHERE codebase_id = ? AND file_path = ?;

After removing stale data, rebuild the FTS table to keep the index consistent:

sql
DROP TABLE IF EXISTS fts_1;
-- Then recreate and repopulate as shown above

Key design points

  • Incremental indexing via file hashes means only changed files are re-parsed and re-embedded, making updates fast even on large codebases.
  • Two search modes — FTS for identifier/keyword lookup and vector search for semantic/natural language queries — cover different use cases. Hybrid search combines both.
  • Weighted FTS (name=5.0, signature=3.0) biases results toward function and type names, which is what developers usually search for.
  • Int8 quantized vectors via vector8() reduce storage by 75% compared to float32 with minimal impact on search quality.
  • Everything runs in a single embedded database file with no external services.

Example

codemogger is an MCP server that implements this pattern as a code indexer for AI coding agents.


File: guides/content-caching.mdx


title: Content Caching

Tools and agents frequently re-read files they have already seen, wasting tokens and time. A content-addressed cache backed by an embedded Turso database can detect unchanged files and return diffs instead of full content, dramatically reducing redundant I/O.

This guide walks through the schema and queries needed to build a file content cache with per-session tracking.

Schema

The cache uses four tables: one for file versions, one for per-session read pointers, and two for statistics.

sql
CREATE TABLE IF NOT EXISTS file_versions (
  path        TEXT NOT NULL,
  hash        TEXT NOT NULL,
  content     TEXT NOT NULL,
  lines       INTEGER NOT NULL,
  created_at  INTEGER NOT NULL,
  PRIMARY KEY (path, hash)
);

CREATE TABLE IF NOT EXISTS session_reads (
  session_id  TEXT NOT NULL,
  path        TEXT NOT NULL,
  hash        TEXT NOT NULL,
  read_at     INTEGER NOT NULL,
  PRIMARY KEY (session_id, path)
);

CREATE TABLE IF NOT EXISTS stats (
  key   TEXT PRIMARY KEY,
  value INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE IF NOT EXISTS session_stats (
  session_id  TEXT NOT NULL,
  key         TEXT NOT NULL,
  value       INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (session_id, key)
);

INSERT OR IGNORE INTO stats (key, value) VALUES ('tokens_saved', 0);

How it fits together

  • file_versions is a content-addressed store. The composite primary key (path, hash) means the same file can have multiple versions stored simultaneously. When a file changes and then changes back (e.g. a branch switch), the old version is already cached.
  • session_reads tracks which version of each file a given session last saw. This is keyed by (session_id, path) so each session independently tracks its own read state.
  • stats and session_stats track cumulative token savings globally and per session.

Connecting

javascript
import { connect } from "@tursodatabase/database";

const db = await connect(".cache/content.db");
await db.exec(SCHEMA); // The CREATE TABLE statements above

Storing a file version

When a file is read for the first time (or has changed), store its content keyed by a hash. INSERT OR IGNORE makes this a no-op if the same version already exists:

sql
INSERT OR IGNORE INTO file_versions (path, hash, content, lines, created_at)
VALUES (?, ?, ?, ?, ?);

The hash should be computed from the file content (e.g. a truncated SHA-256). This makes storage content-addressed — identical content is never stored twice for the same path.

Tracking reads per session

Each session needs to know what it last saw for a given file. On first read, record the session's read pointer:

sql
INSERT OR REPLACE INTO session_reads (session_id, path, hash, read_at)
VALUES (?, ?, ?, ?);

On subsequent reads, check what the session last saw:

sql
SELECT hash FROM session_reads WHERE session_id = ? AND path = ?;

If the stored hash matches the current file hash, the file is unchanged — return a short confirmation instead of the full content. If the hash differs, fetch the old content to compute a diff:

sql
SELECT content FROM file_versions WHERE path = ? AND hash = ?;

Then update the session's read pointer to the new hash:

sql
UPDATE session_reads SET hash = ?, read_at = ? WHERE session_id = ? AND path = ?;

Tracking token savings

Use an atomic counter for global statistics and an upsert for per-session tracking:

sql
-- Global counter
UPDATE stats SET value = value + ? WHERE key = 'tokens_saved';

-- Per-session counter (upsert)
INSERT INTO session_stats (session_id, key, value)
VALUES (?, 'tokens_saved', ?)
ON CONFLICT(session_id, key) DO UPDATE SET value = value + ?;

Querying statistics

sql
-- Number of distinct files cached
SELECT COUNT(DISTINCT path) as file_count FROM file_versions;

-- Global tokens saved
SELECT value FROM stats WHERE key = 'tokens_saved';

-- Tokens saved in the current session
SELECT value FROM session_stats WHERE session_id = ? AND key = 'tokens_saved';

Cleanup

When a file is deleted from disk, remove all its versions and read pointers:

sql
DELETE FROM file_versions WHERE path = ?;
DELETE FROM session_reads WHERE path = ?;

To clear the entire cache:

sql
DELETE FROM file_versions;
DELETE FROM session_reads;
DELETE FROM session_stats;
UPDATE stats SET value = 0;

Key design points

  • Content-addressed storage means branch switches are handled naturally. If you switch from main to a feature branch and back, the original file versions are still in the cache.
  • Session isolation ensures that each consumer independently tracks what it has seen. Session B gets full file content even if Session A already cached it.
  • Hash-based change detection at read time means there is no polling or file watching required for correctness.
  • An embedded Turso database keeps everything in a single file with zero network overhead.

Example

cachebro is an MCP server that implements this pattern as a drop-in file reading cache for AI coding agents.


File: guides/secrets-vault.mdx


title: Secrets Vault

AI coding agents regularly need real credentials — an API token to call GitHub as you, a key to hit your billing provider, a token to deploy. The path of least resistance is to paste the secret into the chat, but then the value lives in the model's context, the transcript, and your provider's logs forever. A secrets vault backed by an encrypted Turso database lets an agent use a secret without ever seeing its value: the secret is injected into a child process, the command runs, and the output is scrubbed before the agent sees it.

This guide covers the schema, the encrypted connection, and the queries for a local secrets vault that an agent can use but never read.

Schema

The vault uses two tables: one for the secrets themselves, one for an append-only audit log.

sql
CREATE TABLE IF NOT EXISTS secrets (
  name         TEXT PRIMARY KEY,
  value        TEXT NOT NULL,
  provider     TEXT,
  account      TEXT,
  environment  TEXT,
  access       TEXT,
  tags         TEXT NOT NULL DEFAULT '',
  description  TEXT,
  created_at   TEXT NOT NULL,
  last_used_at TEXT
);

CREATE TABLE IF NOT EXISTS audit_log (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  ts         TEXT NOT NULL,
  secrets    TEXT NOT NULL,
  command    TEXT NOT NULL,
  cwd        TEXT NOT NULL,
  exit_code  INTEGER,
  outcome    TEXT NOT NULL
);

How it fits together

  • secrets stores each credential keyed by name. The value column holds the secret itself and is only ever read to inject into a child process, never returned to the agent. The metadata columns (provider, account, environment, access, tags) describe a secret without revealing it, so an agent can reason about "the production database token" by its attributes.
  • audit_log is an append-only record of every use: which secrets were injected, the command, the working directory, the exit code, and the outcome (ran or denied).

Connecting

The vault is a single encrypted database file. Turso encrypts every page at rest when you pass a key at connection time, and multiprocess_wal lets several processes (a couple of agent sessions plus a CLI) open the same file safely:

javascript
import { connect } from "@tursodatabase/database";

const db = await connect("vault.db", {
  encryption: { cipher: "aes256gcm", hexkey },
  experimental: ["multiprocess_wal"],
});
await db.exec(SCHEMA); // The CREATE TABLE statements above

hexkey is a 64-character hex string. Where it comes from is up to you. You can derive it from a passphrase with scrypt (and never store it), unwrap it from the OS keychain, or fetch it from a cloud KMS. Turso only needs the key at connect() time; the storage layer is identical regardless of the source.

Storing a secret

Upsert by name so rotating a value keeps the existing metadata:

sql
INSERT INTO secrets (name, value, provider, account, environment, access, tags, description, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
  value       = excluded.value,
  provider    = COALESCE(excluded.provider, secrets.provider),
  account     = COALESCE(excluded.account, secrets.account),
  environment = COALESCE(excluded.environment, secrets.environment),
  access      = COALESCE(excluded.access, secrets.access),
  description = COALESCE(excluded.description, secrets.description);

Listing without exposing values

The query an agent is allowed to run simply never selects the value column:

sql
SELECT name, provider, account, environment, access, tags, description, created_at, last_used_at
FROM secrets ORDER BY name;

This returns secret names and attributes — enough for an agent to pick the right credential — while the value never leaves the database.

Using a secret

To actually use a secret, read its value, inject it into a child process as an environment variable, run the command, and scrub the value out of the output before returning it:

sql
SELECT value FROM secrets WHERE name = ?;

The injection and scrubbing happen in application code, not the database:

javascript
const { value } = await db.prepare(
  "SELECT value FROM secrets WHERE name = ?"
).get([name]);

const result = await runCommand(command, {
  env: { ...process.env, [name]: value },
});

// Replace every occurrence of the value before the agent sees the output
const safe = result.stdout.replaceAll(value, "***");

Scrubbing captured output is not a sandbox. A command that can read a secret can always leak it on purpose (write it to a file, send it over the network). What the vault guarantees is narrower and still useful: the model never receives the value, and a careless echo $TOKEN gets redacted. The secret only ever lives in the encrypted file and the short-lived process that used it.

Auditing every use

Record each access as a row, and update the secret's last_used_at:

sql
INSERT INTO audit_log (ts, secrets, command, cwd, exit_code, outcome)
VALUES (?, ?, ?, ?, ?, ?);

UPDATE secrets SET last_used_at = ? WHERE name = ?;

"Who used the production key, when, and for what" is then an ordinary query:

sql
SELECT ts, secrets, command, cwd, exit_code, outcome
FROM audit_log ORDER BY id DESC LIMIT ?;

Key design points

  • Encrypted at rest, for free. Turso handles whole-database encryption (AES-256-GCM, or one of the faster AEGIS ciphers). The vault never touches a cipher or manages page encryption — it derives a key and hands it to connect().
  • No coordinating daemon. multiprocess_wal lets each agent session and CLI invocation open the same encrypted file directly, so the database is the shared state instead of a long-running server you have to keep alive.
  • The value never appears in a list. Listing is a SELECT that omits the value column; the agent sees names and attributes only.
  • Fail closed. An unknown secret name aborts before the command runs, rather than executing with a missing credential.
  • Auditing is just a table. Every use is an INSERT; reporting on it is a SELECT you can export or alert on.

Example

keymaxxer is an MCP server that implements this pattern as an encrypted secrets vault for AI coding agents.