RippleDB
RippleDB
Guides

Server Setup

Build a production-ready RippleDB server

Server Setup

This guide walks through setting up a RippleDB server with HTTP endpoints for sync.

What You'll Build

  • A Node.js server with /pull and /append endpoints
  • SQLite database with materialization
  • Ready for client sync

Install Dependencies

pnpm add @rippledb/core @rippledb/db-sqlite @rippledb/materialize-db better-sqlite3 expresspnpm add -D typescript @types/better-sqlite3 @types/express tsx
npm install @rippledb/core @rippledb/db-sqlite @rippledb/materialize-db better-sqlite3 expressnpm install -D typescript @types/better-sqlite3 @types/express tsx
yarn add @rippledb/core @rippledb/db-sqlite @rippledb/materialize-db better-sqlite3 expressyarn add -D typescript @types/better-sqlite3 @types/express tsx
pnpm add @rippledb/core @rippledb/db-turso @rippledb/materialize-db @libsql/client expresspnpm add -D typescript @types/express tsx
npm install @rippledb/core @rippledb/db-turso @rippledb/materialize-db @libsql/client expressnpm install -D typescript @types/express tsx
yarn add @rippledb/core @rippledb/db-turso @rippledb/materialize-db @libsql/client expressyarn add -D typescript @types/express tsx
pnpm add @rippledb/core @rippledb/db-drizzle @rippledb/materialize-drizzle drizzle-orm better-sqlite3 expresspnpm add -D typescript @types/better-sqlite3 @types/express tsx
npm install @rippledb/core @rippledb/db-drizzle @rippledb/materialize-drizzle drizzle-orm better-sqlite3 expressnpm install -D typescript @types/better-sqlite3 @types/express tsx
yarn add @rippledb/core @rippledb/db-drizzle @rippledb/materialize-drizzle drizzle-orm better-sqlite3 expressyarn add -D typescript @types/better-sqlite3 @types/express tsx

Define Your Schema

src/schema.ts
import { defineSchema, s, type InferSchema } from "@rippledb/core";

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

export type AppSchema = InferSchema<typeof schema>;

Set Up HLC Generator

src/hlc.ts
import { createHlcState, tickHlc } from "@rippledb/core";
import { randomUUID } from "crypto";

const nodeId = process.env.NODE_ID ?? `server-${randomUUID().slice(0, 8)}`;
const hlcState = createHlcState(nodeId);

export function hlc() {
  return tickHlc(hlcState, Date.now());
}

In a distributed setup, each server instance needs a unique NODE_ID.

Configure the Database

src/db.ts
import { SqliteDb } from '@rippledb/db-sqlite';
import { createSyncMaterializer } from '@rippledb/materialize-db';
import type { AppSchema } from './schema';
import { schema } from './schema';

// Create domain tables
const db = new SqliteDb<AppSchema>({
filename: process.env.DB_PATH ?? './data.db',
schema,
materializer: ({ db, schema }) => {
// Create domain tables if they don't exist
db.exec(`       CREATE TABLE IF NOT EXISTS todos (
        id TEXT PRIMARY KEY,
        title TEXT,
        done INTEGER,
        createdAt TEXT
      );
      CREATE TABLE IF NOT EXISTS users (
        id TEXT PRIMARY KEY,
        name TEXT,
        email TEXT
      );
    `);

    return createSyncMaterializer({
      schema,
      db,
      dialect: 'sqlite',
      tableMap: {
        todos: 'todos',
        users: 'users',
      },
      fieldMap: {
        todos: { id: 'id', title: 'title', done: 'done', createdAt: 'createdAt' },
        users: { id: 'id', name: 'name', email: 'email' },
      },
    });

},
});

export { db };
src/db.ts
import { TursoDb } from '@rippledb/db-turso';
import { createMaterializer } from '@rippledb/materialize-db';
import type { AppSchema } from './schema';
import { schema } from './schema';

const db = new TursoDb<AppSchema>({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
schema,
materializer: ({ db, schema }) =>
createMaterializer({
schema,
db,
dialect: 'sqlite',
tableMap: {
todos: 'todos',
users: 'users',
},
fieldMap: {
todos: { id: 'id', title: 'title', done: 'done', createdAt: 'createdAt' },
users: { id: 'id', name: 'name', email: 'email' },
},
}),
});

export { db };
src/db.ts
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { getTableConfig } from 'drizzle-orm/sqlite-core';
import { DrizzleDb } from '@rippledb/db-drizzle';
import { createDrizzleSyncMaterializer } from '@rippledb/materialize-drizzle';
import type { AppSchema } from './schema';
import { schema } from './schema';

// Drizzle table definitions
export const todosTable = sqliteTable('todos', {
id: text('id').primaryKey(),
title: text('title'),
done: integer('done'),
createdAt: text('createdAt'),
});

const changesTable = sqliteTable('ripple_changes', {
seq: integer('seq').primaryKey({ autoIncrement: true }),
stream: text('stream').notNull(),
change_json: text('change_json').notNull(),
});

const idempotencyTable = sqliteTable('ripple_idempotency', {
stream: text('stream').notNull(),
idempotency_key: text('idempotency_key').notNull(),
last_seq: integer('last_seq').notNull(),
});

const tagsTable = sqliteTable('ripple_tags', {
entity: text('entity').notNull(),
id: text('id').notNull(),
data: text('data').notNull(),
tags: text('tags').notNull(),
deleted: integer('deleted').notNull().default(0),
deleted_tag: text('deleted_tag'),
});

// Initialize SQLite
const sqlite = new Database(process.env.DB_PATH ?? './data.db');
sqlite.exec(`  CREATE TABLE IF NOT EXISTS ripple_changes (...);
  CREATE TABLE IF NOT EXISTS ripple_idempotency (...);
  CREATE TABLE IF NOT EXISTS ripple_tags (...);
  CREATE TABLE IF NOT EXISTS todos (...);`);

const drizzleDb = drizzle(sqlite);

const db = new DrizzleDb<AppSchema, typeof drizzleDb>({
db: drizzleDb,
changesTable,
idempotencyTable,
getTableConfig,
isSync: true,
schema,
materializer: ({ schema }) =>
createDrizzleSyncMaterializer({
schema,
tableMap: { todos: todosTable },
tagsTableDef: tagsTable,
getTableConfig,
fieldMap: { todos: { id: 'id', title: 'title', done: 'done', createdAt: 'createdAt' } },
}),
});

export { db, drizzleDb, todosTable };

Create HTTP Server

src/server.ts
import express from "express";
import { db } from "./db";

const app = express();
app.use(express.json());

// Pull endpoint
app.post("/pull", async (req, res) => {
  try {
    const { stream, cursor, limit } = req.body;
    const result = await db.pull({ stream, cursor, limit });
    res.json(result);
  } catch (error) {
    console.error("Pull error:", error);
    res.status(500).json({ error: "Pull failed" });
  }
});

// Append endpoint
app.post("/append", async (req, res) => {
  try {
    const { stream, idempotencyKey, changes } = req.body;
    const result = await db.append({ stream, idempotencyKey, changes });
    res.json(result);
  } catch (error) {
    console.error("Append error:", error);
    res.status(500).json({ error: "Append failed" });
  }
});

// Health check
app.get("/health", (req, res) => {
  res.json({ status: "ok" });
});

const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Run the Server

npx tsx src/server.ts

Testing the Server

# Append a change
curl -X POST http://localhost:3000/append \
  -H "Content-Type: application/json" \
  -d '{
    "stream": "user-123",
    "changes": [{
      "stream": "user-123",
      "entity": "todos",
      "entityId": "todo-1",
      "kind": "upsert",
      "patch": { "id": "todo-1", "title": "Test", "done": false },
      "tags": { "id": "1000:0:test", "title": "1000:0:test", "done": "1000:0:test" },
      "hlc": "1000:0:test"
    }]
  }'

# Pull changes
curl -X POST http://localhost:3000/pull \
  -H "Content-Type: application/json" \
  -d '{ "stream": "user-123", "cursor": null }'

Production Considerations

Authentication

Add authentication middleware:

import jwt from "jsonwebtoken";

app.use((req, res, next) => {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ error: "Unauthorized" });

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET!);
    next();
  } catch {
    res.status(401).json({ error: "Invalid token" });
  }
});

Stream Authorization

Ensure users can only access their own streams:

app.post("/pull", async (req, res) => {
  const { stream } = req.body;

  // Check stream belongs to user
  if (!stream.startsWith(`user-${req.user.id}`)) {
    return res.status(403).json({ error: "Forbidden" });
  }

  // ... pull logic
});

Error Handling

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: "Internal server error" });
});

Next Steps

On this page