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
/pulland/appendendpoints - 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 tsxnpm install @rippledb/core @rippledb/db-sqlite @rippledb/materialize-db better-sqlite3 expressnpm install -D typescript @types/better-sqlite3 @types/express tsxyarn add @rippledb/core @rippledb/db-sqlite @rippledb/materialize-db better-sqlite3 expressyarn add -D typescript @types/better-sqlite3 @types/express tsxpnpm add @rippledb/core @rippledb/db-turso @rippledb/materialize-db @libsql/client expresspnpm add -D typescript @types/express tsxnpm install @rippledb/core @rippledb/db-turso @rippledb/materialize-db @libsql/client expressnpm install -D typescript @types/express tsxyarn add @rippledb/core @rippledb/db-turso @rippledb/materialize-db @libsql/client expressyarn add -D typescript @types/express tsxpnpm add @rippledb/core @rippledb/db-drizzle @rippledb/materialize-drizzle drizzle-orm better-sqlite3 expresspnpm add -D typescript @types/better-sqlite3 @types/express tsxnpm install @rippledb/core @rippledb/db-drizzle @rippledb/materialize-drizzle drizzle-orm better-sqlite3 expressnpm install -D typescript @types/better-sqlite3 @types/express tsxyarn add @rippledb/core @rippledb/db-drizzle @rippledb/materialize-drizzle drizzle-orm better-sqlite3 expressyarn add -D typescript @types/better-sqlite3 @types/express tsxDefine Your Schema
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
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
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 };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 };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
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.tsTesting 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
- Client Sync — Connect clients to your server
- Materialization — Optimize query performance
- Adapters — Explore database options