RippleDB
RippleDB
Adapters

db-sqlite

SQLite database adapter using better-sqlite3

@rippledb/db-sqlite

A production-ready SQLite adapter for RippleDB using better-sqlite3.

Installation

pnpm add @rippledb/db-sqlite better-sqlite3
npm install @rippledb/db-sqlite better-sqlite3
yarn add @rippledb/db-sqlite better-sqlite3

Basic Usage

import { defineSchema, s } from "@rippledb/core";
import { SqliteDb } from "@rippledb/db-sqlite";

const schema = defineSchema({
  todos: { id: s.string(), title: s.string(), done: s.boolean() },
});

const db = new SqliteDb({
  filename: "./data.db",
  schema,
});

// Append changes
await db.append({
  stream: "user-123",
  changes: [
    /* ... */
  ],
});

// Pull changes
const { changes, nextCursor } = await db.pull({
  stream: "user-123",
  cursor: null,
});

// Close when done
db.close();

With Materialization

To project changes into domain tables, add a materializer:

import { SqliteDb } from "@rippledb/db-sqlite";
import { defineSchema, s } from "@rippledb/core";
import { createSyncMaterializer } from "@rippledb/materialize-db";

const schema = defineSchema({
  todos: { id: s.string(), title: s.string(), done: s.boolean() },
});

const db = new SqliteDb({
  filename: "./data.db",
  schema,
  materializer: ({ db, schema }) =>
    createSyncMaterializer({
      schema,
      db,
      dialect: "sqlite",
      tableMap: { todos: "todos" },
      fieldMap: { todos: { id: "id", title: "title", done: "done" } },
    }),
});

See materialize-db for full configuration options.

Options

filename

Path to the SQLite database file. Use :memory: for an in-memory database.

new SqliteDb({ filename: "./data.db", schema });
new SqliteDb({ filename: ":memory:", schema });

db

Alternatively, pass an existing better-sqlite3 database instance:

import Database from "better-sqlite3";

const sqlite = new Database("./data.db");
const db = new SqliteDb({ db: sqlite, schema });

When using db, RippleDB will not close the connection when you call db.close(). You're responsible for managing the connection lifecycle.

pragmas

SQLite pragmas to apply when creating the database. Only used with filename.

new SqliteDb({
  filename: "./data.db",
  schema,
  pragmas: [
    "journal_mode = WAL",
    "synchronous = NORMAL",
    "cache_size = -64000",
  ],
});

Default: ['journal_mode = WAL']

schema

Schema descriptor used for entity/field discovery and type inference.

materializer

Factory function that returns a materializer adapter. Called with { db, schema }.

import { createSyncMaterializer } from "@rippledb/materialize-db";

new SqliteDb({
  filename: "./data.db",
  schema,
  materializer: ({ db, schema }) =>
    createSyncMaterializer({
      schema,
      db,
      dialect: "sqlite",
      tableMap: { todos: "todos" },
      fieldMap: { todos: { id: "id", title: "title", done: "done" } },
    }),
});

Internal Tables

SqliteDb automatically creates these tables:

-- Change log
CREATE TABLE ripple_changes (
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
  stream TEXT NOT NULL,
  change_json TEXT NOT NULL
);

-- Idempotency tracking
CREATE TABLE ripple_idempotency (
  stream TEXT NOT NULL,
  idempotency_key TEXT NOT NULL,
  last_seq INTEGER NOT NULL,
  PRIMARY KEY (stream, idempotency_key)
);

Using with Drizzle

You can use db-sqlite with Drizzle for your domain queries while RippleDB manages the sync:

import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import { SqliteDb } from "@rippledb/db-sqlite";
import { createDrizzleSyncMaterializer } from "@rippledb/materialize-drizzle";

// Create the underlying SQLite connection
const sqlite = new Database("./data.db");

// Create tables
sqlite.exec(`
  CREATE TABLE IF NOT EXISTS todos (id TEXT PRIMARY KEY, title TEXT, done INTEGER);
  CREATE TABLE IF NOT EXISTS ripple_tags (...);
`);

// Use Drizzle for queries
const drizzleDb = drizzle(sqlite);

// Use SqliteDb for RippleDB sync
const rippleDb = new SqliteDb({
  db: sqlite, // Share the connection
  schema,
  materializer: ({ db, schema }) =>
    createDrizzleSyncMaterializer({
      schema,
      tableMap: { todos: todosTable },
      tagsTableDef: tagsTable,
      getTableConfig,
      // ...
    }),
});

// Now you can use drizzleDb for queries and rippleDb for sync
const todos = drizzleDb.select().from(todosTable).all();

Performance Tips

  1. Enable WAL mode (default): Improves concurrent read performance
  2. Use prepared statements: SqliteDb pre-prepares all statements
  3. Batch changes: Append multiple changes in one call to reduce transaction overhead

Type Safety

SqliteDb is generic over your schema:

import { defineSchema, s, type InferSchema } from "@rippledb/core";

const schema = defineSchema({
  todos: { id: s.string(), title: s.string(), done: s.boolean() },
  users: { id: s.string(), name: s.string() },
});

type MySchema = InferSchema<typeof schema>;

const db = new SqliteDb<MySchema>({ filename: "./data.db", schema });

// Changes are typed
await db.append({
  stream: "user-123",
  changes: [
    {
      entity: "todos", // Must be 'todos' | 'users'
      entityId: "todo-1",
      kind: "upsert",
      patch: { title: "Buy milk" }, // Typed based on entity
      tags: { title: hlc() },
      hlc: hlc(),
    },
  ],
});

On this page