Skip to main content

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:
index.html
<!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: 600px;
      margin: 40px auto;
      padding: 20px;
    }
    input[type="text"] {
      width: 70%;
      padding: 10px;
      font-size: 16px;
    }
    button {
      padding: 10px 20px;
      font-size: 16px;
      cursor: pointer;
    }
    ul {
      list-style: none;
      padding: 0;
    }
    li {
      padding: 10px;
      margin: 5px 0;
      background: #f0f0f0;
      border-radius: 5px;
      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:
app.js
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:
app.js
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:
app.js
// ... 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:
app.js
// ... 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:
app.js
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

1

Open in Browser

Open index.html in your browser. Add some todos.
2

Test Persistence

Refresh the page. Your todos should still be there!
3

Test Real-Time Sync

Open the same page in another tab. Changes in one tab appear instantly in the other.
4

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?

  1. Local-First Storage: GenosDB stores all data locally using OPFS (or IndexedDB as fallback)
  2. Reactive Queries: The db.map() callback fires whenever data changes
  3. P2P Sync: With rtc: true, changes sync automatically via WebRTC
  4. 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!

Build docs developers (and LLMs) love