Skip to content
Back to skills

Tinybase

ASecurity

TinyBase is a reactive in-memory data store for local-first JavaScript and TypeScript apps, with tables, key-value data, queries, persistence and CRDT sync. Use when a user asks to build an offline-capable or local-first app, keep app state in a TinyBase store, persist it to localStorage, IndexedDB, SQLite or Postgres, sync data between devices or tabs with a MergeableStore and WebSockets, or bind a store to React with tinybase/ui-react.

  • 142 stars
  • 0 votes
  • 1 copy
  • 5 views
  • Added September 6, 2026
developmentjavascripttypescriptgojavabashsqlreactnodegitdatabase

Works with

  • terminal
  • cli

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add TerminalSkills/skills --skill tinybase --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Tinybase?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Tinybase
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/terminalskills-tinybase/badge)](https://www.skillsdirectory.com/skills/terminalskills-tinybase)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: tinybase
description: >-
  TinyBase is a reactive in-memory data store for local-first JavaScript and
  TypeScript apps, with tables, key-value data, queries, persistence and CRDT
  sync. Use when a user asks to build an offline-capable or local-first app,
  keep app state in a TinyBase store, persist it to localStorage, IndexedDB,
  SQLite or Postgres, sync data between devices or tabs with a MergeableStore
  and WebSockets, or bind a store to React with tinybase/ui-react.
license: Apache-2.0
compatibility: 'Browsers, Node.js, Bun and Cloudflare Workers; tinybase/ui-react declares React 19 as its peer dependency'
metadata:
  author: terminal-skills
  version: 1.1.0
  category: development
  repository: https://github.com/tinyplex/tinybase
  tags:
    - state-management
    - local-first
    - reactive
    - sync
    - crdt
---

# TinyBase — Reactive Data Store for Local-First Apps

## Overview

TinyBase keeps an app's data in memory as tables (table → row → cell) and key-value pairs, and notifies listeners about exactly what changed. Around that core it offers optional schemas, queries, indexes, relationships and metrics, **Persisters** that load and save the store (browser storage, files, SQLite, PostgreSQL and more), and **Synchronizers** that merge a CRDT-based `MergeableStore` between clients and servers. It has no runtime dependencies; the `store` module alone is about 7 kB gzipped. It is a client-side library, not a hosted database.

This skill targets TinyBase v10 (September 2026).

## Instructions

### Install or scaffold

```bash
npm install tinybase            # add `ws` for a Node sync server, `react` (19) for ui-react

# or generate a complete app (asks for framework, persistence and sync type)
npm create tinybase@latest
npm create tinybase@latest -- --list-options    # machine-readable list of options
```

Core modules come from the `tinybase` root (`createStore`, `createMergeableStore`, `createQueries`, `createRelationships`, `createIndexes`, `createMetrics`, `createCheckpoints`). Every persister, synchronizer and UI binding has its own subpath, for example `tinybase/persisters/persister-indexed-db`.

### Store, schema and listeners

```typescript
import { createStore } from "tinybase";

const store = createStore().setTablesSchema({
  todos: {
    text: { type: "string" },
    done: { type: "boolean", default: false },
    priority: { type: "number", default: 0 },
    categoryId: { type: "string" },
  },
  categories: {
    name: { type: "string" },
    color: { type: "string" },
  },
});

store.setRow("categories", "c1", { name: "Work", color: "#3b82f6" });
store.setRow("todos", "t1", { text: "Build app", priority: 1, categoryId: "c1" });
store.setRow("todos", "t2", { text: "Ship it", priority: 2, categoryId: "c1" });
store.getRow("todos", "t2");   // { text: "Ship it", priority: 2, categoryId: "c1", done: false }

store.setValue("theme", "dark");            // key-value data lives next to the tables

// Listeners fire per table, row or cell; null is a wildcard
const listenerId = store.addRowListener("todos", null, (store, tableId, rowId) => {
  console.log(`Todo ${rowId} changed:`, store.getRow(tableId, rowId));
});
store.setCell("todos", "t1", "done", true);  // logs: Todo t1 changed: { ... done: true }
store.delListener(listenerId);
```

### Queries and relationships

```typescript
import { createQueries, createRelationships } from "tinybase";

const queries = createQueries(store);
queries.setQueryDefinition("activeTodos", "todos", ({ select, join, where }) => {
  select("text");
  select("priority");
  select("categories", "name").as("category");   // column from the joined table
  join("categories", "categoryId");
  where("done", false);
});
queries.getResultTable("activeTodos");
// { t2: { text: "Ship it", priority: 2, category: "Work" } }

// Sorting and paging happen when reading, not in the definition
queries.getResultSortedRowIds("activeTodos", "priority", true, 0, 20);   // descending, first 20

const relationships = createRelationships(store);
relationships.setRelationshipDefinition("todoCategory", "todos", "categories", "categoryId");
relationships.getRemoteRowId("todoCategory", "t1");   // "c1"
relationships.getLocalRowIds("todoCategory", "c1");   // ["t1", "t2"]
```

The query builder provides `select`, `join`, `where`, `group` and `having`. Results are reactive: `queries.addResultTableListener("activeTodos", ...)` fires when the underlying rows change.

### Persistence

```typescript
import { createLocalPersister } from "tinybase/persisters/persister-browser";      // localStorage
import { createIndexedDbPersister } from "tinybase/persisters/persister-indexed-db";

const persister = createIndexedDbPersister(store, "todo-app");
await persister.startAutoPersisting();   // loads first, then saves on every change

// Seed the store only when storage is empty: [tables, values]
// await persister.startAutoPersisting([{ categories: { c1: { name: "Work" } } }, {}]);

await persister.destroy();               // on teardown
```

Other persisters follow the same pattern: `persister-file` (Node), `persister-sqlite-node` (`node:sqlite`), `persister-better-sqlite3`, `persister-sqlite-wasm`, `persister-pg`, `persister-postgres`, `persister-pglite`, `persister-libsql`, `persister-supabase`, `persister-expo-sqlite`. Database persisters store either one JSON blob or map tables to real database tables.

### React bindings

```tsx
import { createStore, createQueries } from "tinybase";
import { createIndexedDbPersister } from "tinybase/persisters/persister-indexed-db";
import {
  Provider, useCell, useCreatePersister, useCreateQueries, useCreateStore,
  useResultSortedRowIds, useSetCellCallback,
} from "tinybase/ui-react";

export function App() {
  const store = useCreateStore(() => createStore());
  const queries = useCreateQueries(store, (store) =>
    createQueries(store).setQueryDefinition("activeTodos", "todos", ({ select, where }) => {
      select("text");
      select("priority");
      where("done", false);
    }),
  );
  useCreatePersister(
    store,
    (store) => createIndexedDbPersister(store, "todo-app"),
    [],
    (persister) => persister.startAutoPersisting(),
  );
  return (
    <Provider store={store} queries={queries}>
      <TodoList />
    </Provider>
  );
}

function TodoList() {
  const ids = useResultSortedRowIds("activeTodos", "priority", true);
  return <ul>{ids.map((id) => <TodoItem key={id} id={id} />)}</ul>;
}

function TodoItem({ id }: { id: string }) {
  const text = useCell("todos", id, "text") as string;   // re-renders only when this cell changes
  const done = useCell("todos", id, "done");
  const toggle = useSetCellCallback("todos", id, "done", () => (cell) => !cell);
  return (
    <li onClick={toggle} style={{ textDecoration: done ? "line-through" : "none" }}>{text}</li>
  );
}
```

Solid and Svelte bindings live in `tinybase/ui-solid` and `tinybase/ui-svelte`.

### Synchronization

Sync needs a `MergeableStore` on every side. `createWsSynchronizer` returns a promise and does not start by itself:

```typescript
import { createMergeableStore } from "tinybase";
import { createWsSynchronizer } from "tinybase/synchronizers/synchronizer-ws-client";

const store = createMergeableStore();
const synchronizer = await createWsSynchronizer(store, new WebSocket("wss://sync.northwind.io/shopping-list"));
await synchronizer.startSync();
// later: await synchronizer.destroy();
```

The URL path (`/shopping-list`) is the room: clients on the same path share data. For tabs of one browser use `createBroadcastChannelSynchronizer(store, "todo-app")` from `tinybase/synchronizers/synchronizer-broadcast-channel` — no server needed.

## Examples

### Example 1: Offline todo app that survives a reload

**User prompt:** "Build a todo list in React that works offline and keeps its data when I refresh the page."

Use the React component above with `createIndexedDbPersister(store, "todo-app")`, and add todos with a row callback:

```tsx
import { useAddRowCallback } from "tinybase/ui-react";

function NewTodo() {
  const addTodo = useAddRowCallback("todos", (text: string) => ({ text, done: false, priority: 1 }));
  return <input placeholder="What needs doing?" onKeyDown={(e) => {
    if (e.key === "Enter") { addTodo(e.currentTarget.value); e.currentTarget.value = ""; }
  }} />;
}
```

Typing "Renew passport" and pressing Enter adds a row with a generated id; the list re-renders, and after a reload the item is still there because `startAutoPersisting()` loaded it from the `todo-app` IndexedDB database before saving resumed.

### Example 2: Sync a shopping list between devices

**User prompt:** "I need two clients to edit the same list and see each other's changes, with the server keeping a copy."

```javascript
// server.mjs — npm install tinybase ws
import { WebSocketServer } from "ws";
import { createMergeableStore } from "tinybase";
import { createFilePersister } from "tinybase/persisters/persister-file";
import { createWsServer } from "tinybase/synchronizers/synchronizer-ws-server";

const server = createWsServer(
  new WebSocketServer({ port: 8048 }),
  (pathId) => createFilePersister(createMergeableStore(), `./data-${pathId.replace(/[^\w-]/g, "_")}.json`),
);
server.addClientIdsListener(null, (server, pathId) =>
  console.log(`${pathId}: ${server.getClientIds(pathId).length} client(s)`),
);
```

```javascript
// client.mjs
import { WebSocket } from "ws";
import { createMergeableStore } from "tinybase";
import { createWsSynchronizer } from "tinybase/synchronizers/synchronizer-ws-client";

const connect = async () => {
  const store = createMergeableStore();
  const synchronizer = await createWsSynchronizer(store, new WebSocket("ws://localhost:8048/shopping-list"));
  await synchronizer.startSync();
  return { store, synchronizer };
};
const phone = await connect();
const laptop = await connect();

phone.store.setRow("items", "milk", { name: "Oat milk", bought: false });
laptop.store.setRow("items", "eggs", { name: "Eggs", bought: false });
laptop.store.setCell("items", "milk", "bought", true);

await new Promise((resolve) => setTimeout(resolve, 300));
console.log(JSON.stringify(phone.store.getTable("items")));
await phone.synchronizer.destroy();    // closes the socket; without it the script never exits
await laptop.synchronizer.destroy();
```

`node server.mjs` logs `shopping-list: 2 client(s)` once both stores are connected; `node client.mjs` prints both rows on the phone store, with the laptop's edit merged: `{"milk":{"name":"Oat milk","bought":true},"eggs":{"name":"Eggs","bought":false}}`. The server writes `data-shopping-list.json`, so a client that connects after a restart receives the same list.

## Guidelines

1. **Load before you save** — `startAutoSave()` on an empty store overwrites what is in storage. Use `startAutoPersisting()`, or `await persister.load()` followed by `startAutoSave()`, never the reverse.
2. **Sync requires `createMergeableStore()`** — passing a plain store to a synchronizer fails with the error `tinybase:0` (codes are listed at tinybase.org/guides/error-codes). A MergeableStore carries CRDT metadata, so use a plain store when nothing is ever merged.
3. **Always `await` and `startSync()`** — `createWsSynchronizer` resolves to a synchronizer that is not yet syncing. Call `destroy()` on unmount; otherwise sockets leak and React strict mode syncs twice.
4. **Raw WebSockets do not reconnect** — wrap the socket with `reconnecting-websocket` in browsers and re-run `synchronizer.load()` then `save()` on `open`.
5. **Queries have no `order`** — sort with `getResultSortedRowIds` / `useResultSortedRowIds`. `queries.setQueryDefinition` builders that call `order()` throw.
6. **Schemas drop, they do not throw** — a cell of the wrong type or an unknown cell is silently discarded and the default applied; check the stored row when a write seems to vanish.
7. **Subscribe narrowly** — `useCell` and `useValue` re-render on one cell; `useTable` re-renders on any change in the table.
8. **The whole store lives in memory** — fine for thousands of rows per user, wrong for an unbounded server-side dataset. Pair it with a real database when the authoritative data is large.
9. **`createLocalPersister` is localStorage** (a few MB, synchronous); use the IndexedDB, OPFS or SQLite-WASM persisters for larger browser data.
10. **v10 removals** — the `persister-sqlite3`, `persister-cr-sqlite-wasm` and `persister-electric-sql` modules are gone. Use `persister-sqlite-node` (`node:sqlite`) or `persister-better-sqlite3`; `persister-libsql` now needs `@libsql/client` 0.18+.
11. **WebSocket sync has no built-in authentication** — anyone who knows the path can read and write the room. Authenticate the upgrade request in your server before handing the socket to TinyBase.

Files in this skill

  • SKILL.md4.9 KB
  • _scores.json1.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…