Repository navigation
Releases: tinyplex/tinybase
Release list
v10.0.0
A Relational Database In The Browser, With TinyJoin
The new persister-tinyjoin module provides the TinyJoinPersister, which saves and loads a Store to and from a TinyJoin database - a tiny, worker-first relational database that runs entirely in the browser:
import {createStore} from 'tinybase';
import {createTinyJoinPersister} from 'tinybase/persisters/persister-tinyjoin';
import {create} from 'tinyjoin';
const tinyJoin = await create('opfs://my-app');
const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
const persister = createTinyJoinPersister(store, tinyJoin, 'my_tinybase');
await persister.save();
console.log((await tinyJoin.query('SELECT * FROM my_tinybase')).rows);
// -> [{_id: '_', store: '[{"pets":{"fido":{"species":"dog"}}},{}]'}]
await persister.destroy();
await tinyJoin.close();Like the other database Persisters, it supports both the JSON and tabular modes, and it follows TinyJoin's own table subscriptions rather than polling for changes. TinyJoin's SQL dialect is deliberately bounded, so read the createTinyJoinPersister function for the two boundaries worth knowing about.
SQL Server and Azure SQL, via mssql
The new persister-mssql module provides the MsSqlPersister, which binds to a SQL Server database with the mssql module. Since Azure SQL Database and Azure SQL Managed Instance speak the same protocol, the same Persister works against all three:
import {connect} from 'mssql';
import {createStore} from 'tinybase';
import {createMsSqlPersister} from 'tinybase/persisters/persister-mssql';
const msSqlPool = await connect('Server=localhost,1433;Database=tinybase');
const msSqlStore = createStore().setTables({pets: {fido: {species: 'dog'}}});
const msSqlPersister = await createMsSqlPersister(
msSqlStore,
msSqlPool,
'my_tinybase',
);
await msSqlPersister.save();
console.log(
(await msSqlPool.request().query('SELECT * FROM my_tinybase;')).recordset,
);
// -> [{_id: '_', store: '[{"pets":{"fido":{"species":"dog"}}},{}]'}]
await msSqlPersister.destroy();
await msSqlPool.request().query('DROP TABLE IF EXISTS my_tinybase;');
await msSqlPool.close();The Persister takes a connection pool that you have already configured, so it stays out of the way of how you authenticate. That matters most on Azure, where Microsoft recommends passwordless access for hosted applications: build the pool with an azure-active-directory-default authentication type and the Persister needs to know nothing about it.
This release supports the JSON serialization mode, for both a Store and a MergeableStore. Tabular mapping may follow.
Automatic loading works differently here than it does for PostgreSQL. There is no equivalent of LISTEN and NOTIFY that is available on every flavor of SQL Server, so the Persister adds a rowversion column to its table and polls it. SQL Server maintains that column itself on every insert and update, so changes made by other writers are still picked up, without needing Service Broker, Change Tracking, or triggers.
A huge thank you to Peter Skoglund (@nltgpeterskoglund) for building all of this - the Persister, the T-SQL dialect handling, the polling, and its tests.
libSQL, With Pooled Connections
The LibSqlPersister now expects v0.18 of the @libsql/client module, which changed the way a local database is opened. A client now keeps a pool of connections rather than a single one, and every command borrows a connection for its duration and returns it afterwards - rolling back any transaction still open on it.
A BEGIN statement issued on its own therefore no longer survives the command that sent it, and so the Persister now runs each write as a transaction session for local file and in-memory databases, just as it always has for remote ones. It previously avoided them, because a session used to open a second connection - and, for an in-memory database, that meant a second and empty database. In v0.18 such a client has a single connection, so that is no longer a concern.
Your own code does not need to change, but your @libsql/client dependency should be v0.18 or later.
A Fuller Agent Skill
The official build-with-tinybase agent skill now ships inside the npm package, at node_modules/tinybase/skills/build-with-tinybase/, so a coding agent can read it locally without fetching it from the web. It remains published at https://tinybase.org/skills/build-with-tinybase/SKILL.md.
It also gained three reference files aimed at the mistakes agents actually make: the import subpath and MergeableStore capability of every Persister, Synchronizer and Schematizer; the lifecycle rules for Persisters and Synchronizers, including WebSocket paths versus channel Ids; and a complete Cloudflare Durable Object recipe covering the Wrangler bindings and migration tag, server-side persistence, and authentication.
The repository is now a Claude Code plugin marketplace, so the skill can be installed directly:
/plugin marketplace add tinyplex/tinybase
/plugin install tinybase@tinybaseA Site You Can Navigate Without A Mouse
The documentation site is now keyboard-navigable throughout. Every page starts with a 'Skip to content' link that becomes visible once focused, the navigation and main content are marked up as labelled landmarks, and focus is now visibly styled wherever it lands.
The color theme toggle cycles automatic, dark, and light as before, but it now announces which mode is active and which one activating it will select, so it is usable from a screen reader rather than by its icon alone.
Breaking Changes in v10.0
There are four, and each affects a single Persister. Three are removals, two of which v9.7 said were coming.
The CR-SQLite Persister Has Been Removed
The persister-cr-sqlite-wasm module has been removed. Unlike the two below, it was not deprecated in v9.7, so this one arrives without notice. The cr-sqlite project it bound to has not had a release since 2024.
It was the odd one out among the Persisters in any case, since it could only ever save a Store and not a MergeableStore, so it could not back a synchronized setup. If you want CRDT-backed sync, TinyBase's own MergeableStore does it natively - see the Using A MergeableStore guide - and the YjsPersister and AutomergePersister remain for the two other external CRDT libraries. For SQLite in a browser, the SqliteWasmPersister and the TinyJoinPersister are both actively supported.
The @vlcn.io/crsqlite-wasm package is no longer an optional peer dependency of TinyBase, so you can drop it from your project when you upgrade.
The sqlite3 Persister Has Been Removed
The persister-sqlite3 module has been removed. The sqlite3 module that it bound to is no longer maintained - its own v6 release marks the repository as such - and SQLite now ships inside Node.js.
The SqliteNodePersister replaces it directly, and the move is close to a rename:
// Before
import {Database} from 'sqlite3';
import {createSqlite3Persister} from 'tinybase/persisters/persister-sqlite3';
const persister = createSqlite3Persister(
store,
new Database(':memory:'),
'my_tinybase',
);// After
import {DatabaseSync} from 'node:sqlite';
import {createSqliteNodePersister} from 'tinybase/persisters/persister-sqlite-node';
const persister = createSqliteNodePersister(
store,
new DatabaseSync(':memory:'),
'my_tinybase',
);Both modes and [MergeableStore](https://tinybase.org/api/mergeable-store/interfaces/mergeable/mergeablest...
v9.7.0
SQLite, via node:sqlite
The new persister-sqlite-node module provides the SqliteNodePersister, which binds to a local SQLite database with the node:sqlite module built into Node.js:
import {DatabaseSync} from 'node:sqlite';
import {createStore} from 'tinybase';
import {createSqliteNodePersister} from 'tinybase/persisters/persister-sqlite-node';
const nodeDb = new DatabaseSync(':memory:');
const nodeStore = createStore().setTables({pets: {fido: {species: 'dog'}}});
const nodePersister = createSqliteNodePersister(
nodeStore,
nodeDb,
'my_tinybase',
);
await nodePersister.save();
console.log(nodeDb.prepare('SELECT * FROM my_tinybase;').all());
// -> [{_id: '_', store: '[{"pets":{"fido":{"species":"dog"}}},{}]'}]
await nodePersister.destroy();
nodeDb.close();Alone amongst the SQLite Persisters, this one needs no third-party dependency at all, since the module ships with Node.js itself. Both JSON and tabular modes are supported, as is persisting a MergeableStore in JSON mode.
Since node:sqlite does not signal when the database changes, automatic loading polls, just as it does for the BetterSqlite3Persister. And note that the module is still marked as experimental in Node.js, so its API may change.
v9.6.0
In Summary
- A new Persister for
pg, the standard PostgreSQL driver for Node.js - and the hosted services, like Neon, compatible with it. - A new Persister for Supabase, which uses its REST API and Realtime rather than a database connection.
- A new Persister for
better-sqlite3, the popular synchronous SQLite module for Node.js. - A new Persister for Capacitor's SQLite plugin, for local databases in native iOS and Android apps.
- MergeableStore support for IndexedDB, closing a gap amongst the browser Persisters.
- SQL built the way each database wants it, which fixes the PowerSyncPersister with PowerSync's Node SDK.
And more!
PostgreSQL, via pg
The new persister-pg module provides the PgPersister, which binds to PostgreSQL databases with the pg module - the de facto standard PostgreSQL driver for Node.js.
It joins the existing PostgresPersister and PglitePersister, and is the one to reach for with hosted services that offer a pg-compatible driver. Neon, for example, has a serverless driver whose Pool and Client objects can be passed straight to the createPgPersister function, so you can persist a Store from an edge runtime that cannot open a TCP connection.
import {Pool} from 'pg';
import {createStore} from 'tinybase';
import {createPgPersister} from 'tinybase/persisters/persister-pg';
const nodePgPool = new Pool({
connectionString: 'postgres://localhost:5432/tinybase',
});
const nodePgStore = createStore().setTables({pets: {fido: {species: 'dog'}}});
const nodePgPersister = await createPgPersister(
nodePgStore,
nodePgPool,
'my_tinybase',
);
await nodePgPersister.save();
console.log((await nodePgPool.query('SELECT * FROM my_tinybase;')).rows);
// -> [{_id: '_', store: '[{"pets":{"fido":{"species":"dog"}}},{}]'}]
await nodePgPool.query('UPDATE my_tinybase SET store = $1 WHERE _id = $2;', [
'[{"pets":{"felix":{"species":"cat"}}},{}]',
'_',
]);
await nodePgPersister.load();
console.log(nodePgStore.getTables());
// -> {pets: {felix: {species: 'cat'}}}
await nodePgPersister.destroy();
await nodePgPool.query('DROP TABLE my_tinybase;');
await nodePgPool.end();Both JSON and tabular modes are supported, as is reactive auto-loading, and a MergeableStore can be persisted in JSON mode. There's more information in the documentation for the new persister-pg module.
Supabase
Also new is the persister-supabase module, which provides the SupabasePersister (as requested in issue #204). Pass the createSupabasePersister function the client you get back from Supabase's createClient function, and your Store is persisted to a table in your project:
import {createClient} from '@supabase/supabase-js';
import {createStore} from 'tinybase';
import {createSupabasePersister} from 'tinybase/persisters/persister-supabase';
const supabase = createClient('https://my-project.supabase.co', 'anon-key');
const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
const persister = createSupabasePersister(store, supabase, 'my_tinybase');
await persister.save();
await persister.startAutoLoad();Unlike the other PostgreSQL Persisters, this one goes through Supabase's REST API rather than connecting to the database. So it runs in a browser or edge runtime, your row-level security policies apply to what it reads and writes, and the startAutoLoad method hears about other clients' changes over Supabase Realtime instead of polling. In return, only the JSON serialization mode is available, since the REST API cannot run the arbitrary SQL that tabular mapping needs.
It also issues no DDL at all, unlike its siblings. Where they create tables, add and drop columns as Cells come and go, and install their own change-notification triggers, this one touches nothing but a single row. You create the table and its policies yourself and they stay exactly as you left them, and the persister-supabase module documentation has the SQL to do it.
SQLite, via better-sqlite3
The new persister-better-sqlite3 module provides the BetterSqlite3Persister, which binds to a local SQLite database with the popular synchronous better-sqlite3 module:
import Database from 'better-sqlite3';
import {createBetterSqlite3Persister} from 'tinybase/persisters/persister-better-sqlite3';
const betterDb = new Database(':memory:');
const betterStore = createStore().setTables({pets: {fido: {species: 'dog'}}});
const betterPersister = createBetterSqlite3Persister(
betterStore,
betterDb,
'my_tinybase',
);
await betterPersister.save();
console.log(betterDb.prepare('SELECT * FROM my_tinybase;').all());
// -> [{_id: '_', store: '[{"pets":{"fido":{"species":"dog"}}},{}]'}]
await betterPersister.destroy();
betterDb.close();Since better-sqlite3 does not signal when the database changes underneath it, automatic loading polls. There's more information in the documentation for the new persister-better-sqlite3 module.
SQLite In Capacitor
The new persister-capacitor-sqlite module provides the CapacitorSqlitePersister, which binds to a SQLite database in a Capacitor app via the @capacitor-community/sqlite plugin (as requested in issue #219). Both JSON and tabular modes work, as does persisting a MergeableStore in JSON mode.
import {CapacitorSQLite, SQLiteConnection} from '@capacitor-community/sqlite';
import {createStore} from 'tinybase';
import {createCapacitorSqlitePersister} from 'tinybase/persisters/persister-capacitor-sqlite';
const sqlite = new SQLiteConnection(CapacitorSQLite);
const db = await sqlite.createConnection('my.db', false, 'no-encryption', 1, false);
await db.open();
const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
const persister = createCapacitorSqlitePersister(store, db, 'my_tinybase');
await persister.save();Note that this module's tests run against a mocked plugin, since it needs a native iOS or Android runtime that a Node test suite cannot provide. Its SQL behavior is shared with the other SQLite Persisters and well covered by them, but the binding to the plugin itself is not exercised on a device, so please report anything that behaves differently in a real app.
MergeableStore Support For IndexedDB
The IndexedDbPersister can now persist a MergeableStore, closing a gap that had made IndexedDB the odd one out amongst the browser Persisters (as requested in issue #203). The SessionPersister, LocalPersister and OpfsPersister could all already do this; now the browser's most capable storage can too.
import {createMergeableStore} from 'tinybase';
import {createIndexedDbPersister} from 'tinybase/persisters/persister-indexed-db';
const store = createMergeableStore().setTables({pets: {fido: {species: 'dog'}}});
const persister = createIndexedDbPersister(store, 'petStore');
await persister.save();A regular Store continues to use the 't' and 'v' object stores exactly as before, and a [MergeableStore](https://t...
v9.5
Schema Enums
CellSchema and ValueSchema can now use an enum property instead of type to allow only specific primitive values (as requested in issue #38). Enums must be non-empty, can mix strings, finite numbers, and booleans, and continue to use allowNull when null is also valid.
import {createStore} from 'tinybase';
const enumStore = createStore().setValuesSchema({
status: {enum: ['available', 'adopted'], default: 'available'},
rating: {enum: ['good', 5, true], allowNull: true},
});
enumStore.setValues({status: 'adopted', rating: true});
enumStore.setValue('status', 'missing');
console.log(enumStore.getValues());
// -> {status: 'available', rating: true}Each schema entry must use exactly one of type or enum. Defaults are used only when they are enum members, or are null when null is allowed. The schema-aware Store APIs infer exact unions from enum members, and the Zod, Valibot, ArkType, Effect Schema, TypeBox, and Yup schematizers now preserve supported primitive enum and literal constraints in the schemas they produce.
Schema Type Unions
CellSchema and ValueSchema can now accept a non-empty array in the type property to list one or more allowed broad types (as requested in issue #223):
const unionStore = createStore().setValuesSchema({
reference: {type: ['string', 'number'], default: 'unknown'},
response: {type: ['boolean', 'object'], allowNull: true},
});
unionStore.setValues({reference: 42, response: {accepted: true}});
unionStore.setValue('reference', false);
console.log(unionStore.getValues());
// -> {reference: 'unknown', response: {accepted: true}}A type array can contain string, number, boolean, object, and array. Multiple type names form a union, and repeated names have no additional effect. null continues to be represented by allowNull: true, and defaults are used only when they match one of the listed types. Each schema entry still uses exactly one of type or enum: a type array allows every value of its listed types, while an enum allows only its listed values.
The schema-aware Store APIs infer the corresponding TypeScript unions. The ArkType, Effect Schema, TypeBox, Valibot, and Zod schematizers also preserve supported broad type unions, while continuing to preserve literal-only unions as exact enums.
Schema-Aware Type Fixes
This release also corrects a number of declarations on the /with-schemas type surface. These are type-level fixes only, and require no changes to your runtime code:
- The six schematizer factory functions were previously exported from their
/with-schemasentry points as types rather than as values, so they could not actually be called from those entry points. They are now exported correctly, both individually and from the tinybase/schematizers/with-schemas module: the createArkTypeSchematizer function, createEffectSchematizer function, createTypeBoxSchematizer function, createValibotSchematizer function, createYupSchematizer function, and createZodSchematizer function. - The
extraCellsBeforeandextraCellsAfterprops were missing from the schema-aware declarations of the DOM table components, and are now available in the React, Solid, and Svelte packages. - ResultCell was declared as
string | number | boolean | null, and so did not admit the object and array Cells introduced in v8.0. It is now simply Cell, and the Aggregate, AggregateAdd, AggregateRemove, AggregateReplace, and Having types have been aligned to match. - The
requestIdparameter of the Send and Receive types is now correctly IdOrNull rather than Id, matching what synchronizer implementations are actually passed.
v9.4.0
selectAll
Queries can now select every Cell present in a root or joined Row with the selectAll keyword (as requested in issue #40). Heterogeneous Rows are supported directly: each result Row contains only its source Row's Cells, while the result Table's Cell Ids are their reactive union.
Joined selections retain their Cell Ids by default, or can take a Cell Id prefix or mapping callback. Selection clauses are applied in declaration order, and source Cell Ids within a selectAll function call are processed lexically, so collision behavior is deterministic.
import {createQueries, createStore} from 'tinybase';
const selectAllStore = createStore().setTable('pets', {
fido: {species: 'dog', color: 'brown'},
felix: {species: 'cat', indoor: true},
});
const selectAllQueries = createQueries(selectAllStore);
selectAllQueries.setQueryDefinition('allPets', 'pets', ({selectAll}) =>
selectAll(),
);
console.log(selectAllQueries.getResultRow('allPets', 'fido'));
// -> {color: 'brown', species: 'dog'}
console.log(selectAllQueries.getResultRow('allPets', 'felix'));
// -> {indoor: true, species: 'cat'}Ungrouped queries iterate only the Cells present in each matched Row. Grouped queries discover columns from the table-wide Cell Id union, but materialize each Row in the same deterministic order before aggregation. They rebuild when that union changes; selected Cells that are not aggregated remain grouping dimensions.
Reliability And Hardening
- Multiplexed WebSockets accept at most 100 active channels, with channel Ids limited to 1,024 UTF-8 bytes. Client creation rejects local overflow without closing the shared socket; servers also bound pending channel setup and teardown. Fragment and startup-buffer limits are shared across the physical socket, abandoned fragments and in-flight subscriptions are cleaned up, and teardown waits for every channel even if one fails.
startAutoPersistingrolls back both partially started halves if the second half fails, without replacing the original error or stopping a newer concurrent start.- Fragmented Durable Object SQL storage now encodes unsafe and uppercase prefixes collision-free. Previous ambiguous table names are not adopted automatically and must be migrated explicitly.
- PostgreSQL tabular persistence keeps distinct INSERT, DELETE, and UPDATE notification triggers even when table names exceed PostgreSQL's identifier limit.
- Release validation now uses clean installs, runs the full unit and executable documentation suite with PostgreSQL in CI, and tests the final assembled distribution before publishing it.
v9.3.0
TinyBase v9.3 lets multiple independently synchronized MergeableStores share one WebSocket connection. It also includes broad reliability and hardening work across core data, synchronization, persistence, UI integrations, packaging, and performance.
Multiple Stores Over One WebSocket
Multiple WebSocket Synchronizers can now share a single physical WebSocket connection (#177). This is useful when an application has several independently synchronized MergeableStore instances but needs to limit its connection count.
Create the WebSocket with the tinybase subprotocol and provide a channel Id as the third argument to each createWsSynchronizer call:
import {createMergeableStore} from 'tinybase';
import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';
import {createWsServer} from 'tinybase/synchronizers/synchronizer-ws-server';
import {WebSocket, WebSocketServer} from 'ws';
const multistoreServer = createWsServer(new WebSocketServer({port: 8052}));
const multistoreWebSocket = new WebSocket(
'ws://localhost:8052/petShop',
'tinybase',
);
const petsSynchronizer = await createWsSynchronizer(
createMergeableStore(),
multistoreWebSocket,
'pets',
);
const employeesSynchronizer = await createWsSynchronizer(
createMergeableStore(),
multistoreWebSocket,
'employees',
);
console.log(petsSynchronizer.getWebSocket() == multistoreWebSocket);
// -> true
console.log(employeesSynchronizer.getWebSocket() == multistoreWebSocket);
// -> true
await petsSynchronizer.destroy();
console.log(multistoreWebSocket.readyState == WebSocket.OPEN);
// -> true
await employeesSynchronizer.destroy();
await multistoreServer.destroy();Each channel extends the base URL path, so the example uses the logical paths petShop/pets and petShop/employees. Legacy clients can connect directly to those full paths, while omitting the channel Id retains the existing signature and wire protocol. Multiplexing is supported by WsServer and WsServerSimple; WsServerDurableObject continues to use one URL path and Durable Object per WebSocket.
Channel Ids are not an authorization boundary: a client accepted on a base path can subscribe to any valid descendant channel. Authenticate and isolate untrusted clients by base path, and do not treat client Ids derived from Sec-WebSocket-Key as authenticated identities.
Reliability And Hardening
Breaking-ish Changes
- Errors use compact numeric codes.
- Failed transactions now rollback.
- Certain previously accepted/reserved or unserializable values are rejected.
- Client-only Solid and Svelte exports now correctly fail server resolution.
- Solid's UndoOrRedoInformation changed from values to accessors.
- Over-eager runtime exports disappeared from the Svelte package.
The full explanations for these and many other changes are below:
Core Data And APIs
- Arbitrary Ids such as proto, constructor, and toString are now safe throughout Store, MergeableStore, synchronization, and persistence.
- Errors from transaction actions and pre-commit callbacks now roll back content, schemas, MergeableStore stamps and hashes, and temporary state across Store, MergeableStore, and Checkpoints; nested failures roll back the shared outer transaction, while post-commit listener failures no longer strand the Store.
- Query definitions are staged before commit, and new Index, Metric, Query, and Relationship definitions are discarded if their Ids listener throws.
- Deleting a Query definition now releases cached pre- and result Stores once nothing still references them.
- MergeableStore now atomically rejects HLCs that are not exactly 16 characters or are over five minutes in the future, carries overflowing 24-bit counters into wall-clock time, and verifies local stamps without mutating rejected caller payloads.
- String Cells, Values, and schema defaults using TinyBase's reserved leading U+FFFD or exact U+FFFC encodings are rejected, while invalid or schema-incompatible persisted encodings are ignored safely.
- Object and array Cells and Values now have an explicit JSON-compatible content contract, reject unserializable data, and no longer modify caller-owned or frozen containers during bulk writes.
- Queries and Indexes now group, sort, and index equivalent object and array values consistently without stale results; direct rich Index keys remain distinct from custom-function arrays that select multiple Slices.
- TablesSchema and ValuesSchema objects are cloned before normalization, making frozen schemas reusable and preserving the previous schema after an invalid replacement; schema JSON getters also preserve object and array defaults.
- Middleware receives cloned object and array values in public JavaScript form, with callback results validated and encoded only at the Store boundary.
- Middleware and Checkpoints now clean up Store registrations safely, avoid duplicate listeners after recreation, and skip phantom checkpoints for structurally unchanged rich content.
Synchronization
- WsServer and WsServerSimple now share multiplex negotiation, decoding, channel-lifecycle, and cleanup behavior, and retain safe error listeners for their lifetimes.
- Synchronizers reject pending requests on transport failure, remove built-in listeners when destroyed, and expose transport failures to custom register callbacks.
- BroadcastChannelSynchronizer validates message envelopes, while LocalSynchronizer snapshots scheduled recipients and cancels deliveries when destroyed.
- WebSocket fragments now split by UTF-8 byte size at code-point boundaries and use at most 1,000 fragments.
- Malformed WebSocket traffic reports error 14 and closes the offending peer with status 1007 before relay; complete messages and multiplex envelopes are limited to 16 MiB, with oversize input closing only its sender.
- Offline and startup queues, pending requests, incomplete fragments, and socket buffering are bounded; queued traffic expires or coalesces, while overload reports error 15, closes peers with status 1013 where appropriate, and stops accepting new requests when the pending map is full.
- Multiplexed channels keep independent timeout and error handling, clear failed subscriptions for retry, and settle immediately around closing or closed sockets; reconnect handshakes and queued replays cannot consume replacement connection state.
- WsServer setup, teardown, Persister retries, path resubscription, and destruction now clean up deterministically; subscriptions are acknowledged before persisted startup, destroy closes clients and awaits the WebSocketServer, and stale path cleanup cannot remove replacements.
- All path and client Id listeners still run when one throws, listener and ignored-error failures no longer strand server state, and [WsServerSimple](https://tinybase.org/api/synchro...
v9.2.0
TinyBase v9.2 makes the library easier for coding agents and AI systems to discover, evaluate, understand, and use correctly.
Although there are no changes to TinyBase source code, this is more than just an AI-specific documentation pass. The package and website metadata now describes TinyBase using the concrete problems it solves: reactive in-memory data, offline and local-first applications, persistence, CRDTs, and synchronization. This helps search and retrieval systems connect an application requirement to TinyBase before the library name is already known.
Agent Documentation And Retrieval
The website now publishes a concise llms.txt orientation file and a fuller llms-full.txt agent guide. A new Why TinyBase? guide explains when TinyBase is a good architectural fit, when it is not, and which Store, persistence, synchronization, and UI pieces to start with.
The repository also includes configuration for current documentation to be indexed by Context7, as well as machine-readable structured metadata for ordinary search and retrieval systems.
An Official TinyBase Skill
The repository now contains an official build-with-tinybase agent skill. It guides coding agents through architecture selection, scaffolding, safe extension of generated applications, and behavioral verification.
The skill treats persistence and synchronization as behaviors to prove, not just code that compiles. It instructs agents to verify persisted state across a real reload and synchronized state between multiple clients.
Agent-Friendly Scaffolding
The create-tinybase scaffolding tool now exposes its current options as JSON, documents its non-interactive mode, and generates an architecture-specific AGENTS.md in every new project. This gives agents a deterministic path from application requirements to a complete, working TinyBase app.
Agents can inspect the current generator contract with:
npm create tinybase@latest -- --list-optionsThe generated guidance records the chosen language, UI framework, persistence, synchronization, and schema options, identifies the important files, and states the verification expected for that architecture.
If you're a human, you shouldn't notice or mind these changes. If you're an agent, we hope this makes it easier to find and use TinyBase correctly. Either way, let us know how it goes!
v9.1.0
Required Schema Fields
Schemas can now mark a Cell or Value as required without providing a default value (#173). Full writes now reject Rows or Values that omit a required non-default field, while preserving the existing rule that a default also implies 'requiredness' (and will be filled automatically if omitted).
import {createStore} from 'tinybase';
const requiredSchemaStore = createStore().setTablesSchema({
pets: {
species: {type: 'string', required: true},
sold: {type: 'boolean', default: false},
},
});
requiredSchemaStore.setRow('pets', 'fido', {species: 'dog'});
console.log(requiredSchemaStore.getRow('pets', 'fido'));
// -> {species: 'dog', sold: false}
requiredSchemaStore.setRow('pets', 'felix', {});
console.log(requiredSchemaStore.hasRow('pets', 'felix'));
// -> false
console.log(requiredSchemaStore.getRow('pets', 'felix'));
// -> {}Custom Sorting
This release adds custom sorting for sorted Row Id APIs, so applications can opt into numeric or domain-specific ordering without changing TinyBase's existing alphanumeric default behavior.
The getSortedRowIds method and addSortedRowIdsListener method now support a custom sorter function in the SortedRowIdsArgs object. The useSortedRowIds hook and getSortedRowIds Svelte function also accept the sorter positionally (#190, #213).
const numericSortStore = createStore();
['1', '10', '2'].forEach((rowId) =>
numericSortStore.setRow('pets', rowId, {sold: false}),
);
const numericRowIdSorter = (sortKey1, sortKey2) =>
Number(sortKey1) - Number(sortKey2);
console.log(
numericSortStore.getSortedRowIds({
tableId: 'pets',
sorter: numericRowIdSorter,
}),
);
// -> ['1', '2', '10']Breaking change: In the ui-solid and ui-svelte modules, the positional Store argument for useSortedRowIds and getSortedRowIds respectively has moved one slot later to make room for a positional custom sorter. If you pass a Store as the final positional argument, add undefined before it, or switch to the object argument form.
Index Presence Helpers
The Indexes interface already exposes the hasIndex method and hasSlice method. This release also adds reactive helpers for UI integrations: useHasIndex and useHasSlice for React and Solid, and hasIndex and hasSlice for Svelte (#163). The Indexes interface also now exposes a addHasIndexListener method and addHasSliceListener method.
v9.0
This release has no new features; just fixes and reliability improvements.
TinyBase v9.0 is all about addressing issues from the community - and making local-first apps behave better in production. The areas addressed include persistence, synchronization, schema defaults, infrastructure limits, and edge-case query semantics.
There is one new configuration option (for more selective Value persistence), but otherwise the wider theme is reliability. This release hardens WebSocket synchronization, Durable Object storage, PowerSync startup, custom Persister loading, and a few type and documentation edges so that apps recover and sync more cleanly under real-world conditions.
We hope you enjoy using TinyBase and if you find further issues, keep them coming!
Persistence Subsets
This release adds finer-grained configuration for tabular database Persisters, allowing Values persistence to be limited to selected Value Ids (#279).
For apps that keep durable state and UI-only state in the same Store, the DpcTabularValues load and save properties can now use an array of Value Ids instead of a simple boolean:
const valuesSubsetDatabasePersisterConfig = {
mode: 'tabular',
values: {
load: ['selectedPet', 'open'],
save: ['selectedPet'],
},
};
console.log(valuesSubsetDatabasePersisterConfig.values.load);
// -> ['selectedPet', 'open']When a subset is configured, unlisted Values in the Store are not saved, and unlisted columns in the Values database table are left untouched.
WebSocket Synchronization Fixes
WebSocket Synchronizers can now fragment large synchronization payloads and reassemble them on receipt. This helps deployments behind infrastructure with WebSocket message size limits, such as Cloudflare Workers and Durable Objects (#261).
The createWsSynchronizer and createWsServer functions now accept an optional fragment size argument. Incomplete fragment buffers expire using the existing request timeout, which can also now be set on createWsServer. Durable Object servers can override the getFragmentSize and getRequestTimeoutSeconds methods to set the same behavior for messages they send.
The WebSocket Synchronizer documentation now also clarifies that WsServer paths come from WebSocket URL paths, not MergeableStore Ids, so clients that need separate synchronization groups should connect to different URL paths (#206).
When a persisted WsServer path starts after having no connected clients, it now loads its persisted Store before starting synchronization. This means the first client to reconnect is sent only the data it is missing, instead of receiving the whole persisted Store as a fresh change (#205).
Schema Default Synchronization Fixes
Schema defaults inserted automatically into MergeableStores now use neutral timestamps, so defaulted Values and Cells no longer overwrite newer synced data from another peer. Explicit writes of default values still receive normal timestamps (#167).
PowerSync Persistence Fixes
The PowerSync Persister now updates existing tabular rows before inserting missing ones, instead of replacing whole rows during upserts. This avoids flooding PowerSync upload queues with replacement writes when schema validation causes loaded data to be written back unchanged on startup (#262).
Custom Persister Loading Fixes
Custom Persisters can now return undefined from getPersisted to indicate that there is no persisted content. Loading then uses initialContent if it was provided, or otherwise leaves the Store unchanged without invoking the ignored error handler (#161).
Durable Object Persistence Fixes
The Durable Object SQL Storage Persister's fragmented mode now stores table row data as one SQL row per TinyBase Row, instead of one SQL row per Cell. This reduces the number of SQLite writes for wide Rows while preserving the fragmented mode's protection from Cloudflare's 2MB row limit. Existing cell-level fragmented data is still loaded and is cleaned up when the Row is next saved (#268).
Query Transaction Fixes
Grouped queries, including those with having clauses, now correctly return their current result when a query definition is added during an active Store transaction (#259).
Query Documentation Clarifications
The TinyQL documentation now explicitly describes that a Row only appears in a query result when at least one selected Cell or calculated value is defined. If all selected values for a Row resolve to undefined, no ResultRow is created for that Row (#183).
Type Fixes
The schema-aware MergeableContent, MergeableChanges, persisted content, and Persister listener types now validate content being set or loaded in the same way as Store setters. This catches invalid Cell or Value Ids and values in custom Persisters and MergeableStore setters (#178).
Breaking Change
This release is a major version because the Durable Object SQL Storage Persister's fragmented mode uses a new storage layout. TinyBase v9.0 can read the old cell-level fragmented data written by earlier releases, but once it saves the new row-level fragmented data, older TinyBase versions are not designed to read that data back. Apps using fragmented Durable Object SQL storage should not roll those Durable Objects back to an earlier TinyBase version after v9.0 has written to them.
Thank You
Thanks to everyone whose reports and fixes shaped this release:
Dheeraj, Jakub Riedl, Patryk Wegrzyn, Damilola Romniyi, Andrew Glago, wattroll, Will Honey, and Daniel Berndt.
Couldn't do it without you!
v8.5.0
React Chart Components
This release adds the new ui-react-dom-charts module, providing reactive SVG chart components for React apps.
The LineChart component and BarChart component can render data directly from a Store Table, or from a Queries ResultTable, using the same Provider context patterns as the rest of the React UI modules. For more complex charts, the CartesianChart component can compose multiple LineSeries component children and BarSeries component children in one SVG.
A chart can be bound to a Table with just the Table Id and the Cell Ids to use for the x and y values:
import React from 'react';
import {createRoot as createReactRoot} from 'react-dom/client';
import {createStore} from 'tinybase';
import {
CartesianChart,
LineChart,
LineSeries,
} from 'tinybase/ui-react-dom-charts';
const chartStore = createStore();
const app = document.createElement('div');
const root = createReactRoot(app);
chartStore.setTable('sales', {
jan: {month: 'Jan', order: 1, profit: 4, revenue: 12},
feb: {month: 'Feb', order: 2, profit: 7, revenue: 18},
mar: {month: 'Mar', order: 3, profit: 5, revenue: 15},
});
const MyChart = () => (
<LineChart
tableId="sales"
store={chartStore}
xCellId="month"
yCellId="revenue"
sortCellId="order"
/>
);
root.render(<MyChart />);
console.log(app.firstChild?.nodeName.toLowerCase());
// -> 'svg'To plot multiple series in the same chart, use the CartesianChart component as the shared frame and declare each child series explicitly:
const MyCompositeChart = () => (
<CartesianChart tableId="sales" store={chartStore}>
<LineSeries
className="revenue"
label="Revenue"
xCellId="month"
yCellId="revenue"
sortCellId="order"
/>
<LineSeries
className="profit"
label="Profit"
xCellId="month"
yCellId="profit"
sortCellId="order"
/>
</CartesianChart>
);
const compositeChartApp = document.createElement('div');
createReactRoot(compositeChartApp).render(<MyCompositeChart />);
console.log(compositeChartApp.querySelectorAll('.line-series').length);
// -> 2The same CartesianChart frame can include zero or one XAxis component and YAxis component child. These configuration children let you override inferred axis titles, numeric bounds, explicit ticks, tick counts, tick formatters, and axis-specific class names without adding more top-level chart props.
The Axis Overrides demo shows this pattern with numeric timestamps formatted as dates on the x axis, and revenue ticks formatted as dollar amounts on the y axis.
The Time Axes demo focuses on date handling directly, showing ISO date strings that infer a time scale and Unix second timestamps that use scale="time" and timestampUnit="second".
Chart presentation is handled with CSS. The chart components emit stable SVG class names for grid lines, axes, data marks, and tooltips, so you can keep data binding in props and visual styling in stylesheets.
Read more in the Using Charts guide and the Chart Components (React) demos.
The create-tinybase CLI tool also now includes a Charting app option, so you can scaffold a complete editable table with reactive chart output by running npm create tinybase@latest.
There are no intended breaking changes in this release. Please try the new chart components and let us know which chart types or styling hooks would be most useful next.