jotaBase
Your app reads and writes to a local store, instantly. Sync when you want — with your server, or with everyone else using the app.
There is a public sample database — 240 tasks and the people they’re assigned to. No signup, and no key: one database is published read-only, so a request with no credential is answered.
npm install @jotabase/client dexie
import { createClient } from "@jotabase/client";
const jb = createClient({ url: "https://api.jotabase.com" });
const db = jb.db("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();
Read-only: writes come back denied. Or run npx jotabase demo to
print this snippet in your terminal.
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.
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
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 } });
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();
// Pull everything since the last checkpoint.
await db.sync();
// And re-sync whenever the server changes.
db.subscribe(() => render());
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
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");
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,
})));
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.