Tablinum

2 min read Original article ↗

Local-first database
for the browser

Encrypted sync. Built-in collaboration. Zero backend.

Device A

Device B

Buy coffee beans

Fix login bug

Buy coffee beans

Fix login bug

Nostr Relay

Every record is gift-wrapped with NIP-59 encryption. The relay stores and forwards encrypted blobs — it never sees your data.

Everything you need

A complete toolkit for local-first applications

Local First

Your app works offline. All data lives on your device in IndexedDB. No server required, no loading spinners.

Encrypted Sync

Data syncs to Nostr relays, encrypted end-to-end with NIP-59 gift wrapping. Relay operators cannot read your data.

Built-in Collaboration

Share databases with invite links. Key rotation on member removal — like changing locks when a roommate moves out.

Typed Collections

Define schemas with collection() and field.*() builders. Full TypeScript inference from schema to query results.

Svelte 5 Integration

First-class reactive bindings using async runes. Queries inside $derived(await ...) auto-update when data changes.

Configurable Logging

Effect-based structured logging with timing spans. From full debug output to complete silence in one config line.

Two APIs, one database

Use the Effect API for full control, or the Svelte API for reactive simplicity

src/lib/db.ts

import { Tablinum, collection, field } from "tablinum/svelte";

const schema = {
  todos: collection("todos", {
    title: field.string(),
    done: field.boolean(),
  }, { indices: ["done"] }),
};

export const db = new Tablinum({
  schema,
  relays: ["wss://relay.example.com"],
});

export const todos = db.collection("todos");

src/routes/+page.svelte

<script lang="ts">
  import { todos } from "$lib/db";

  let title = $state("");
  let pending = $derived(await todos.where("done").equals(false).get());

  async function addTodo() {
    await todos.add({ title, done: false });
    title = "";
  }
</script>

<form onsubmit={addTodo}>
  <input bind:value={title} placeholder="Add a todo..." />
  <button type="submit">Add</button>
</form>

{#each pending as todo (todo.id)}
  <div>{todo.title}</div>
{/each}

How it works

Store locally

Data lives in IndexedDB on your device. Reads are instant, writes never block.

Encrypt

Every record is wrapped in NIP-59 gift wrapping. Relays see encrypted blobs, nothing more.

Sync via relays

NIP-77 negentropy figures out what's missing and only transfers the difference.

Available everywhere

Your data arrives on every device and every collaborator, fully decrypted and ready to use.

Get started in minutes