Quickstart Tutorial
In this tutorial, you’ll build a real-time todo application that syncs automatically across multiple browser tabs and devices. No backend server required!
What You’ll Build
A todo app with:
Real-time synchronization across tabs and devices
Persistent local storage
Automatic conflict resolution
Reactive UI updates
Step 1: Set Up Your HTML
Create an index.html file:
<! DOCTYPE html >
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > GenosDB Todo App </ title >
< style >
body {
font-family : Arial , sans-serif ;
max-width : 600 px ;
margin : 40 px auto ;
padding : 20 px ;
}
input [ type = "text" ] {
width : 70 % ;
padding : 10 px ;
font-size : 16 px ;
}
button {
padding : 10 px 20 px ;
font-size : 16 px ;
cursor : pointer ;
}
ul {
list-style : none ;
padding : 0 ;
}
li {
padding : 10 px ;
margin : 5 px 0 ;
background : #f0f0f0 ;
border-radius : 5 px ;
display : flex ;
justify-content : space-between ;
align-items : center ;
}
.completed {
text-decoration : line-through ;
opacity : 0.6 ;
}
</ style >
</ head >
< body >
< h1 > Real-Time Todo App </ h1 >
< div >
< input type = "text" id = "taskInput" placeholder = "Enter a new task" >
< button id = "addBtn" > Add </ button >
</ div >
< ul id = "taskList" ></ ul >
< script type = "module" src = "app.js" ></ script >
</ body >
</ html >
Step 2: Initialize GenosDB
Create an app.js file and initialize the database:
import { gdb } from 'https://cdn.jsdelivr.net/npm/genosdb@latest/dist/index.min.js' ;
// Initialize database with P2P sync enabled
const db = await gdb ( 'todo-app' , { rtc: true });
console . log ( 'Database initialized!' );
The rtc: true option enables real-time peer-to-peer synchronization. Remove it if you only need local storage.
Step 3: Store and Query Data
Let’s add the ability to create and list todos:
import { gdb } from 'https://cdn.jsdelivr.net/npm/genosdb@latest/dist/index.min.js' ;
const db = await gdb ( 'todo-app' , { rtc: true });
const taskInput = document . getElementById ( 'taskInput' );
const addBtn = document . getElementById ( 'addBtn' );
const taskList = document . getElementById ( 'taskList' );
// Add a new todo
async function addTodo ( text ) {
await db . put ({
type: 'todo' ,
text: text ,
completed: false ,
timestamp: Date . now ()
});
taskInput . value = '' ; // Clear input
}
// Listen for button click
addBtn . addEventListener ( 'click' , () => {
const text = taskInput . value . trim ();
if ( text ) addTodo ( text );
});
// Listen for Enter key
taskInput . addEventListener ( 'keypress' , ( e ) => {
if ( e . key === 'Enter' ) {
const text = taskInput . value . trim ();
if ( text ) addTodo ( text );
}
});
Step 4: Real-Time Subscriptions
Now let’s make the UI update automatically when todos change:
// ... previous code ...
// Track rendered todos to avoid duplicates
const renderedTodos = new Map ();
// Subscribe to real-time todo updates
await db . map (
{
query: { type: 'todo' },
field: 'timestamp' ,
order: 'asc'
},
({ id , value , action }) => {
if ( action === 'added' || action === 'initial' ) {
renderTodo ( id , value );
} else if ( action === 'updated' ) {
updateTodo ( id , value );
} else if ( action === 'removed' ) {
removeTodo ( id );
}
}
);
// Render a todo item
function renderTodo ( id , todo ) {
if ( renderedTodos . has ( id )) return ;
const li = document . createElement ( 'li' );
li . id = id ;
li . innerHTML = `
<span class=" ${ todo . completed ? 'completed' : '' } "> ${ todo . text } </span>
<div>
<button onclick="toggleTodo(' ${ id } ')">Toggle</button>
<button onclick="deleteTodo(' ${ id } ')">Delete</button>
</div>
` ;
taskList . appendChild ( li );
renderedTodos . set ( id , li );
}
// Update a todo item
function updateTodo ( id , todo ) {
const li = renderedTodos . get ( id );
if ( li ) {
const span = li . querySelector ( 'span' );
span . textContent = todo . text ;
span . className = todo . completed ? 'completed' : '' ;
}
}
// Remove a todo item
function removeTodo ( id ) {
const li = renderedTodos . get ( id );
if ( li ) {
li . remove ();
renderedTodos . delete ( id );
}
}
The action parameter tells you whether a todo was added, updated, or removed. The initial action fires for existing todos when you first subscribe.
Step 5: Toggle and Delete
Add functions to toggle completion and delete todos:
// ... previous code ...
// Toggle todo completion
window . toggleTodo = async ( id ) => {
const { result } = await db . get ( id );
if ( result ) {
await db . put (
{ ... result . value , completed: ! result . value . completed },
id
);
}
};
// Delete a todo
window . deleteTodo = async ( id ) => {
await db . remove ( id );
};
Complete Code
Here’s the full app.js file:
import { gdb } from 'https://cdn.jsdelivr.net/npm/genosdb@latest/dist/index.min.js' ;
const db = await gdb ( 'todo-app' , { rtc: true });
const taskInput = document . getElementById ( 'taskInput' );
const addBtn = document . getElementById ( 'addBtn' );
const taskList = document . getElementById ( 'taskList' );
const renderedTodos = new Map ();
// Add a new todo
async function addTodo ( text ) {
await db . put ({
type: 'todo' ,
text: text ,
completed: false ,
timestamp: Date . now ()
});
taskInput . value = '' ;
}
// Event listeners
addBtn . addEventListener ( 'click' , () => {
const text = taskInput . value . trim ();
if ( text ) addTodo ( text );
});
taskInput . addEventListener ( 'keypress' , ( e ) => {
if ( e . key === 'Enter' ) {
const text = taskInput . value . trim ();
if ( text ) addTodo ( text );
}
});
// Subscribe to real-time updates
await db . map (
{
query: { type: 'todo' },
field: 'timestamp' ,
order: 'asc'
},
({ id , value , action }) => {
if ( action === 'added' || action === 'initial' ) {
renderTodo ( id , value );
} else if ( action === 'updated' ) {
updateTodo ( id , value );
} else if ( action === 'removed' ) {
removeTodo ( id );
}
}
);
// Render functions
function renderTodo ( id , todo ) {
if ( renderedTodos . has ( id )) return ;
const li = document . createElement ( 'li' );
li . id = id ;
li . innerHTML = `
<span class=" ${ todo . completed ? 'completed' : '' } "> ${ todo . text } </span>
<div>
<button onclick="toggleTodo(' ${ id } ')">Toggle</button>
<button onclick="deleteTodo(' ${ id } ')">Delete</button>
</div>
` ;
taskList . appendChild ( li );
renderedTodos . set ( id , li );
}
function updateTodo ( id , todo ) {
const li = renderedTodos . get ( id );
if ( li ) {
const span = li . querySelector ( 'span' );
span . textContent = todo . text ;
span . className = todo . completed ? 'completed' : '' ;
}
}
function removeTodo ( id ) {
const li = renderedTodos . get ( id );
if ( li ) {
li . remove ();
renderedTodos . delete ( id );
}
}
// Global functions for onclick handlers
window . toggleTodo = async ( id ) => {
const { result } = await db . get ( id );
if ( result ) {
await db . put (
{ ... result . value , completed: ! result . value . completed },
id
);
}
};
window . deleteTodo = async ( id ) => {
await db . remove ( id );
};
Test Your App
Open in Browser
Open index.html in your browser. Add some todos.
Test Persistence
Refresh the page. Your todos should still be there!
Test Real-Time Sync
Open the same page in another tab. Changes in one tab appear instantly in the other.
Test P2P Sync
Open the page on another device on the same network. Todos sync automatically!
Open the browser console to see GenosDB logs. You can monitor peer connections and sync activity.
What Just Happened?
Local-First Storage : GenosDB stores all data locally using OPFS (or IndexedDB as fallback)
Reactive Queries : The db.map() callback fires whenever data changes
P2P Sync : With rtc: true, changes sync automatically via WebRTC
Conflict Resolution : If two peers edit the same todo, Last-Write-Wins (LWW) resolves it
Next Steps
CRUD Operations Learn all the database operations in depth
Queries Master advanced query operators and filters
P2P Setup Configure custom relays and TURN servers
Graph Traversal Use the $edge operator for recursive queries
Live Examples
Explore more working examples:
Troubleshooting
Todos don’t persist
Make sure you’re using a modern browser with OPFS support (Chrome 86+, Firefox 111+, Safari 15.2+).
P2P sync not working
Check that both peers are using the same database name ('todo-app'). WebRTC may also be blocked by some firewalls.
Todos appear multiple times
Ensure you’re checking if (renderedTodos.has(id)) return; in your render function to avoid duplicates.
Have questions? Join the discussion on GitHub !