Skip to content

Building Your First Event-Sourced App

Let's build a real todo app from scratch using DeltaBase

Time to build something real. We’re going to create a todo app that actually does event sourcing properly. No fake demos with fake data. A real app you could ship.

By the end of this tutorial, you’ll have:

  • A working todo app with proper event sourcing
  • Real-time updates when todos change
  • Complete audit trail of every action
  • Understanding of DeltaBase core concepts

Should take about 20 minutes. Let’s go.

Create a new directory and initialize it:

Terminal window
mkdir deltabase-todo
cd deltabase-todo
npm init -y

Install DeltaBase and a web framework:

Terminal window
npm install @delta-base/server express cors
npm install -D @types/node @types/express @types/cors tsx nodemon

Create your package.json scripts:

{
"scripts": {
"dev": "nodemon --exec tsx src/server.ts",
"build": "tsc",
"start": "node dist/server.js"
}
}

Create src/deltabase.ts:

import { DeltaBase } from '@delta-base/server';
export const deltabase = new DeltaBase({
apiKey: process.env.DELTABASE_API_KEY || 'your-api-key-here',
baseUrl: process.env.DELTABASE_URL || 'https://api.delta-base.com'
});
export const eventStore = deltabase.getEventStore('todos');
export const eventBus = deltabase.getEventBus('todos');

Events are facts. Things that happened. Create src/events.ts:

// Todo events - notice they're past tense
export type TodoCreated = {
type: 'todo.created';
data: {
id: string;
title: string;
description?: string;
};
};
export type TodoCompleted = {
type: 'todo.completed';
data: {
id: string;
completedAt: string;
};
};
export type TodoUncompleted = {
type: 'todo.uncompleted';
data: {
id: string;
};
};
export type TodoUpdated = {
type: 'todo.updated';
data: {
id: string;
title?: string;
description?: string;
};
};
export type TodoEvent = TodoCreated | TodoCompleted | TodoUncompleted | TodoUpdated;

Event sourcing means building current state by replaying events. Create src/todo.ts:

import { TodoEvent } from './events';
export interface Todo {
id: string;
title: string;
description?: string;
completed: boolean;
createdAt: string;
completedAt?: string;
}
export function buildTodoFromEvents(events: TodoEvent[]): Todo | null {
if (events.length === 0) return null;
// Start with creation event
const createEvent = events.find(e => e.type === 'todo.created');
if (!createEvent) return null;
let todo: Todo = {
id: createEvent.data.id,
title: createEvent.data.title,
description: createEvent.data.description,
completed: false,
createdAt: new Date().toISOString() // You'd get this from event metadata
};
// Apply all subsequent events
for (const event of events) {
switch (event.type) {
case 'todo.completed':
todo.completed = true;
todo.completedAt = event.data.completedAt;
break;
case 'todo.uncompleted':
todo.completed = false;
todo.completedAt = undefined;
break;
case 'todo.updated':
if (event.data.title) todo.title = event.data.title;
if (event.data.description !== undefined) {
todo.description = event.data.description;
}
break;
}
}
return todo;
}

Now let’s build an API that speaks events. Create src/server.ts:

import express from 'express';
import cors from 'cors';
import { eventStore } from './deltabase';
import { TodoEvent } from './events';
import { buildTodoFromEvents } from './todo';
const app = express();
app.use(cors());
app.use(express.json());
// Create a todo
app.post('/todos', async (req, res) => {
const { title, description } = req.body;
const id = `todo-${Date.now()}`;
const event: TodoEvent = {
type: 'todo.created',
data: { id, title, description }
};
try {
await eventStore.appendToStream(id, [event]);
res.json({ id, message: 'Todo created' });
} catch (error) {
res.status(500).json({ error: 'Failed to create todo' });
}
});
// Get a specific todo
app.get('/todos/:id', async (req, res) => {
try {
const events = await eventStore.readStream<TodoEvent>(req.params.id);
const todo = buildTodoFromEvents(events);
if (!todo) {
return res.status(404).json({ error: 'Todo not found' });
}
res.json(todo);
} catch (error) {
res.status(500).json({ error: 'Failed to get todo' });
}
});
// Complete a todo
app.post('/todos/:id/complete', async (req, res) => {
const event: TodoEvent = {
type: 'todo.completed',
data: {
id: req.params.id,
completedAt: new Date().toISOString()
}
};
try {
await eventStore.appendToStream(req.params.id, [event]);
res.json({ message: 'Todo completed' });
} catch (error) {
res.status(500).json({ error: 'Failed to complete todo' });
}
});
// Uncomplete a todo
app.post('/todos/:id/uncomplete', async (req, res) => {
const event: TodoEvent = {
type: 'todo.uncompleted',
data: { id: req.params.id }
};
try {
await eventStore.appendToStream(req.params.id, [event]);
res.json({ message: 'Todo uncompleted' });
} catch (error) {
res.status(500).json({ error: 'Failed to uncomplete todo' });
}
});
// Update a todo
app.patch('/todos/:id', async (req, res) => {
const { title, description } = req.body;
const event: TodoEvent = {
type: 'todo.updated',
data: {
id: req.params.id,
...(title && { title }),
...(description !== undefined && { description })
}
};
try {
await eventStore.appendToStream(req.params.id, [event]);
res.json({ message: 'Todo updated' });
} catch (error) {
res.status(500).json({ error: 'Failed to update todo' });
}
});
// List all todos
app.get('/todos', async (req, res) => {
try {
const streams = await eventStore.listStreams({ limit: 100 });
const todos = [];
for (const streamId of streams) {
const events = await eventStore.readStream<TodoEvent>(streamId);
const todo = buildTodoFromEvents(events);
if (todo) todos.push(todo);
}
res.json(todos);
} catch (error) {
res.status(500).json({ error: 'Failed to list todos' });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Todo API running on port ${PORT}`);
});

Start your server:

Terminal window
npm run dev

Test it with curl:

Terminal window
# Create a todo
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"title": "Learn DeltaBase", "description": "Build a real app"}'
# Get the todo
curl http://localhost:3000/todos/todo-1640995200000
# Complete it
curl -X POST http://localhost:3000/todos/todo-1640995200000/complete
# List all todos
curl http://localhost:3000/todos

Actually pretty impressive:

  1. Event-sourced architecture - Every change is an event
  2. Complete audit trail - You can see every action ever taken
  3. Time travel - Replay events to any point in time
  4. Immutable data - Events never change, only accumulate
  5. Conflict resolution - Built-in optimistic concurrency control

Your todo app works, but you can make it better:

  • Add real-time updates with WebSockets
  • Build read models for faster queries
  • Add user authentication and multi-tenancy
  • Create a web frontend
  • Add more complex business logic

The beauty of event sourcing is that you can add these features without breaking existing functionality. Your events are your single source of truth.

Ready to ship something real? You just learned the foundations.