jotaBase

An offline-first backend, built on the edge.

Your app reads and writes to a local store, instantly. Sync when you want — with your server, or with everyone else using the app.

What it does

Try it first

@jotabase/sample is a public database — 240 tasks and the people they’re assigned to. No signup, no key, nothing to configure.

npm install @jotabase/client dexie
import { createClient } from "@jotabase/client";

const jb = createClient();

const db = jb.db("@jotabase/sample");
await db.open();
await db.sync();                          // pulls 246 documents

const overdue = await db.query("tasks")
  .where(t => t.status !== "done")
  .order("dueAt")
  .limit(10)
  .toArray();

Public means read-only: writes come back denied. You can publish one of your own the same way — see below. Or run npx jotabase demo to print this snippet in your terminal.

Quick start

  1. Create a database

    One command, no dashboard. It registers you if needed, creates the database, issues a publishable key and writes .env with the right prefix for your bundler.

    npx jotabase register     # or: npx jotabase login
    npx jotabase create notes
    VITE_JOTABASE_URL=https://api.jotabase.com
    VITE_JOTABASE_PUBLISHABLE_KEY=pk_live_...

    A publishable key is safe to ship in a frontend bundle. A secret key never is, and belongs on a server only. Prefer clicking? The dashboard does the same thing.

  2. Connect

    import { createClient } from "@jotabase/client";
    
    const jb = createClient({
      url: import.meta.env.VITE_JOTABASE_URL,
      publishableKey: import.meta.env.VITE_JOTABASE_PUBLISHABLE_KEY,
    });
    
    const db = jb.db("notes");
    await db.open();          // loads the saved checkpoint
  3. Write

    The local write lands first and is pushed in the background, so it survives a dropped connection.

    await db.put({ id: "note-1", collection: "notes", data: { title: "Hello", done: false } });
  4. Read

    Queries run against the local copy, so they answer offline and without a round trip.

    const open = await db.query("notes")
      .where({ done: false })
      .order("title")
      .toArray();
  5. Sync

    // Pull everything since the last checkpoint.
    await db.sync();
    
    // And re-sync whenever the server changes.
    db.subscribe(() => render());
  6. Sign your users in (optional)

    Everything above works with the key alone. Add end-user auth when your users need roles, or rows only they can see.

    const { token } = await jb.auth("notes").signIn({ email, password });
    jb.setToken(token);       // syncs now run as this user, with their roles

Working with the data

Filtering, sorting, pagination and grouping all run on the device. A query returns rows — the document’s data with its id merged in.

const tasks = db.query("tasks");

// Filter — a predicate, or {field: value}. Repeated calls are ANDed.
await tasks.where({ team: "platform" }).where(t => t.points >= 5).toArray();

// Sort, with a tie-breaker. Missing values sort last, both directions.
await tasks.order("priority").order("dueAt", "desc").toArray();

// Paginate. count() ignores the page, so you can size the pager.
const rows  = await tasks.order("dueAt").page(3, 20).toArray();
const total = await tasks.count();

// Group and aggregate.
await tasks.countBy("status");          // { todo: 42, doing: 49, done: 49, ... }
await tasks.groupBy("assignee");
await tasks.where({ status: "done" }).sum("points");
await tasks.distinct("priority");

Share a database

Any database can be made readable by anyone, at @handle/name — no key to hand out and nothing to rotate.

npx jotabase alias jardel     # claim your handle, once per account
npx jotabase rules notes --read-only
npx jotabase publish notes    # now at @jardel/notes
const db = createClient().db("@jardel/notes");
await db.sync();              // no key, no account

Load data you already have

A CSV goes up from the terminal. Row ids come from the line number, so re-importing a corrected file updates rows instead of duplicating them.

npx jotabase import notes tasks.csv --collection tasks
// Or in the browser — putMany is put for a batch.
await db.putMany(rows.map((row, i) => ({
  id: `import-${i + 1}`,
  collection: "tasks",
  data: row,
})));

Status

jotaBase is in active development. Registration is open, the API is live, and the client is on npm as @jotabase/client — but it is pre-1.0 and the API may still change.