Releases: tinyplex/tinybase
Release list
v9.3.0-beta.1
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
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 applies the same outbound safety checks as WsServer.
- WsServerDurableObject reports an empty path befor...
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.
v8.4.2
This release updates dependencies, including the resolution for a breaking Svelte 5.56 issue.
v8.4.1
This release addresses a small z-index issue with the Inspector components.
v8.4.0
Solid DOM Components And Inspector
This release completes TinyBase's Solid support with two new additions: the ui-solid-dom module and the ui-solid-inspector module.
The ui-solid-dom module provides browser-ready Solid components for rendering and editing TinyBase data as HTML tables. They mirror the React DOM components, but use Solid components and Accessors throughout.
Alongside the table components, the new ui-solid-inspector module brings the TinyBase development inspector to Solid apps too, making it easy to inspect and edit Stores, Indexes, Relationships, and Queries during development:
A small Solid app can use both modules together:
import {render} from 'solid-js/web';
import {createRoot} from 'solid-js';
import {createStore} from 'tinybase';
import {CellView, Provider, useCell} from 'tinybase/ui-solid';
import {TableInHtmlTable} from 'tinybase/ui-solid-dom';
import {Inspector} from 'tinybase/ui-solid-inspector';
const domStore = createStore().setTable('pets', {
fido: {species: 'dog', color: 'brown'},
felix: {species: 'cat', color: 'black'},
});
const inspectorStore = createStore().setTable('pets', {
fido: {species: 'dog'},
});
const appSolid = document.createElement('div');
const disposeAppSolid = render(
() => (
<>
<TableInHtmlTable tableId="pets" store={domStore} editable={true} />
<Provider store={inspectorStore}>
<Inspector open={true} />
</Provider>
</>
),
appSolid,
);
console.log(appSolid.querySelectorAll('tbody tr').length);
// -> 2
console.log(typeof Inspector);
// -> 'function'
disposeAppSolid();Read more in the Using Solid DOM Components guide and the Inspector (Solid) demo.
New Solid Demos
This release also adds a complete set of Solid UI component demos, plus a Countries demo and an Inspector demo, so you can see the new modules working across Stores, Indexes, Relationships, Queries, and editable views.
These demos intentionally mirror the React set where possible, making it easier to compare implementation patterns across frameworks.
There are no intended breaking changes in this release. If you spot any issues with the new Solid DOM components or the Solid inspector, please let us know!
v8.3.0
Solid Support
This release adds the new ui-solid module, bringing TinyBase's reactive Store bindings to Solid apps. It provides Solid primitives that return Accessor functions, listener primitives that clean up with Solid's lifecycle, Provider context helpers, and view components for rendering Store data directly in a Solid component tree.
The primitives follow Solid's fine-grained reactivity model. They read TinyBase data immediately, then update the Accessor when the underlying Store data changes:
import {createRoot} from 'solid-js';
import {createStore} from 'tinybase';
import {useCell} from 'tinybase/ui-solid';
const solidStore = createStore().setCell('pets', 'fido', 'color', 'brown');
createRoot((dispose) => {
const color = useCell('pets', 'fido', 'color', solidStore);
console.log(color());
// -> 'brown'
dispose();
});The module also includes Solid view components and a Provider component, so you can assemble UI directly from TinyBase data while still taking advantage of Solid's selective updates:
import {render} from 'solid-js/web';
import {CellView} from 'tinybase/ui-solid';
const solidApp = document.createElement('div');
document.body.appendChild(solidApp);
const disposeSolid = render(
() => (
<CellView tableId="pets" rowId="fido" cellId="color" store={solidStore} />
),
solidApp,
);
console.log(solidApp.textContent);
// -> 'brown'
disposeSolid();Read more in the Building UIs With Solid guides and the ui-solid module documentation.
A huge thanks to Daniel Grant, @djgrant, for designing and implementing this new Solid support! Please let us know how it works for you and if you have any feedback or suggestions.
Queries From Queries
Also in this release, TinyQL queries can now use the result of another query as their source. This lets you build complex results in small, readable steps - for example, first finding all dogs, and then querying that result to find just the brown ones:
import {createQueries} from 'tinybase';
const queryStore = createStore().setTable('pets', {
fido: {species: 'dog', color: 'brown'},
felix: {species: 'cat', color: 'black'},
cujo: {species: 'dog', color: 'black'},
});
const queryQueries = createQueries(queryStore)
.setQueryDefinition('dogs', 'pets', ({select, where}) => {
select('color');
where('species', 'dog');
})
.setQueryDefinition('brownDogs', true, 'dogs', ({select, where}) => {
select('color');
where('color', 'brown');
});
console.log(queryQueries.getResultTable('brownDogs'));
// -> {fido: {color: 'brown'}}The true argument tells setQueryDefinition that dogs is another query result, not a Table in the underlying Store. This works in query clauses too, so you can select or join from query results as your TinyQL definitions become more modular.
Schematizer Enums
This release addresses enum types in the schematizers. For example, Zod enums are now schematized as string types, rather than being rejected as invalid. This is also now documented accordingly.
There are no intended breaking changes in this release. If you try the new Solid bindings in particular, please let us know how they fit your expectations of building Solid apps - and good luck!
v8.2.0
Svelte DOM Components
This release completes TinyBase's Svelte support with two new additions: the ui-svelte-dom module and the ui-svelte-inspector module.
The ui-svelte-dom module provides browser-ready Svelte components for rendering and editing TinyBase data as HTML tables. They mirror the React DOM components, but use Svelte component composition and props throughout:
<script>
import {createStore} from 'tinybase';
import {TableInHtmlTable} from 'tinybase/ui-svelte-dom';
const store = createStore().setTable('pets', {
fido: {species: 'dog', color: 'brown'},
felix: {species: 'cat', color: 'black'},
});
</script>
<TableInHtmlTable tableId="pets" {store} editable />Svelte Inspector
Alongside the table components, the new ui-svelte-inspector module brings the TinyBase development inspector to Svelte apps too, making it easy to inspect and edit Stores, Indexes, Relationships, and Queries during development:
<script>
import {createStore} from 'tinybase';
import {Provider} from 'tinybase/ui-svelte';
import {Inspector} from 'tinybase/ui-svelte-inspector';
const store = createStore().setTable('pets', {
fido: {species: 'dog'},
});
</script>
<Provider {store}>
<Inspector />
</Provider>Read more in the Using Svelte DOM Components guide and the Inspecting Data guide.
New Demos
This release also adds a complete set of Svelte UI component demos, plus an Inspector demo, so you can see the new modules working across Stores, Indexes, Relationships, Queries, and editable views.
These demos intentionally mirror the React set where possible, making it easier to compare implementation patterns across frameworks.
There are no intended breaking changes in this release. If you spot any issues with the new Svelte DOM components or the Svelte inspector, please let us know!