Skip to content

Commit c52ecd4

Browse files
committed
edits and video embed
1 parent 523e927 commit c52ecd4

File tree

1 file changed

+48
-34
lines changed

1 file changed

+48
-34
lines changed

src/blog/tanstack-db-0.6-app-ready-with-persistence-and-includes.md

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: TanStack DB 0.6 Now Includes Persistence, Offline Support, and Hierarchical Data
33
excerpt: TanStack DB 0.6 adds SQLite-backed persistence across runtimes, hierarchical includes for projecting normalized data into UI-shaped trees, reactive effects, virtual props for sync state, and more.
4-
published: 2026-03-19
4+
published: 2026-03-25
55
draft: true
66
authors:
77
- Sam Willis
@@ -10,14 +10,14 @@ authors:
1010

1111
![Persistence, Offline Support, and Hierarchical Data](/blog-assets/tanstack-db-0.6-app-ready-with-persistence-and-includes/header.jpg)
1212

13-
TanStack DB 0.6 is the release that lands some highly anticipated features that many of you have been asking for, making it a lot more ergonomic for app development.
13+
TanStack DB 0.6 is the release that brings some highly anticipated features many of you have been asking for, making it much more ergonomic for app development.
1414

15-
You can now project normalized data into the same hierarchical structure as your UI. You can optionally persist local state with a SQLite-backed persistence layer across runtimes. You can trigger reactive side effects from live queries. You can build outbox views and WhatsApp-style delivery indicators directly from row metadata. And a few APIs that used to rely on implicit magic are now getting more explicit and uniform.
15+
You can now project normalized data into the same hierarchical structure as your UI. You can optionally persist local state with a SQLite-backed persistence layer across runtimes. You can trigger reactive side effects from live queries. You can build outbox views and WhatsApp-like delivery indicators directly from row metadata. And a few APIs that used to rely on implicit magic are now explicit and uniform.
1616

1717
Here is what shipped:
1818

19-
- [Persisted local state](#persisted-local-state) with an optional SQLite-backed persistence layer across browser, React Native, Expo, Node, Electron, Capacitor, Tauri, and Cloudflare Durable Objects
20-
- [Includes](#includes-project-your-data-into-the-same-shape-as-your-ui) for projecting normalized data into the same hierarchical structure as your UI. Similar to GraphQL, but without the need for new infrastructure.
19+
- [Persistent local state](#persistent-local-state) with adapters for SQLite persistence across browser, React Native, Expo, Node, Electron, Capacitor, Tauri, and Cloudflare Durable Objects
20+
- [Includes](#includes-project-your-data-into-the-same-shape-as-your-ui) for projecting normalized data into the hierarchical structure of your UI. Similar to GraphQL, but without the need for new infrastructure.
2121
- [`createEffect`](#createeffect-reactive-side-effects-for-workflows-tools-and-agents) for workflows, side effects, and agent-style automation
2222
- [Virtual props](#virtual-props-outboxes-delivery-state-and-row-provenance) like `$synced` and `$origin` for outbox views, sync indicators, and provenance-aware queries
2323
- [`queryOnce`](#queryonce) for one-shot queries using the same query language as live queries
@@ -33,28 +33,36 @@ Finally, we are also putting out [a call for server-side rendering (SSR) design
3333

3434
One of the best examples of what 0.6 unlocks is our React Native shopping list demo.
3535

36-
> Demo video embed coming soon.
36+
<iframe
37+
width="100%"
38+
style="aspect-ratio: 16/9;"
39+
src="https://www.youtube.com/embed/EBXOjQds8hU"
40+
title="TanStack DB 0.6 Shopping List Demo"
41+
frameborder="0"
42+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
43+
allowfullscreen
44+
></iframe>
3745
38-
It starts up from persisted SQLite state through `op-sqlite`, projects normalized data into a hierarchical UI shape with [includes](#includes-project-your-data-into-the-same-shape-as-your-ui), and still keeps TanStack DB's fine-grained reactivity underneath. But the really important thing is what that persistence unlocks when you pair it with [`@tanstack/offline-transactions`](https://github.com/TanStack/db/tree/main/packages/offline-transactions).
46+
It starts from persisted SQLite state through `op-sqlite`, projects normalized data into a hierarchical UI shape with [includes](#includes-project-your-data-into-the-same-shape-as-your-ui), and still keeps TanStack DB's fine-grained reactivity underneath. But the really important thing is what that persistence unlocks when you pair it with [`@tanstack/offline-transactions`](https://github.com/TanStack/db/tree/main/packages/offline-transactions).
3947

4048
TanStack DB already had the query engine, transaction model, optimistic updates, and the offline transaction API. Persistence was the missing piece. Once local state is durable, that stack can add up to something fully local-first instead of only feeling local while the app is open.
4149

4250
### More than local-first
4351

44-
Persistence is the feature people asked for, but it does not define TanStack DB. The core idea is simpler: put a real query engine and transaction engine on the client, and let storage and synchronization live wherever they belong. Local-first is one configuration of that. Server-authoritative with fast optimistic updates is another. The same primitives support both.
52+
Persistence is the feature people asked for, but it does not define TanStack DB. The core idea is simpler: put a real transactional query engine on the client, and let storage and synchronization live wherever they belong. Local-first is one configuration of that. Server-authoritative with fast optimistic updates is another. Both are supported by the same primitives.
4553

46-
## Persisted local state
54+
## Persistent local state
4755

4856
Persistence is the biggest practical unlock in 0.6.
4957

50-
We have wanted a persistence story for a while, and a lot of you have asked for it too. The problem space was always broader than just "save some rows to disk":
58+
We wanted a persistence layer for a while, and a lot of you have asked for it too. The problem space was always broader than just "save some rows to disk":
5159

5260
- persistence is not only about faster startup
5361
- it needs to compose with synced remote state and optimistic local state
5462
- it needs to work across multiple runtimes
5563
- it needs to support large datasets without assuming everything lives in memory
5664
- it needs to work across multiple tabs and windows
57-
- and it needs a sane story for schema evolution
65+
- and it needs a sane approach to schema evolution
5866

5967
That led us to a pragmatic choice: **use SQLite as the persistence layer**.
6068

@@ -65,10 +73,10 @@ That gives TanStack DB one persistence model that can span:
6573
- Node
6674
- Electron
6775
- Tauri
68-
- Capacitior
76+
- Capacitor
6977
- Cloudflare Durable Objects
7078

71-
Instead of inventing a different storage story for each environment, we can keep one persistence model and swap in runtime-specific adapters. The result is optional persisted local state that can enable a local-first application, without limiting TanStack DB to only local-first use cases.
79+
Instead of implementing a different storage layer for each environment, we can keep one persistence model and swap in runtime-specific adapters. The result is optional persistent local state that enables local-first applications, without limiting TanStack DB to local-first use cases.
7280

7381
For synced collections, persistence does **not** change the source of truth. The server is still authoritative. Persistence gives you a durable local base to start from quickly, work against offline, and then reconcile back to the upstream source of truth when sync resumes.
7482

@@ -114,7 +122,7 @@ export const shoppingItemsCollection = createCollection(
114122
)
115123
```
116124

117-
That gives you a durable local base for a synced collection. Pair it with `@tanstack/offline-transactions`, and you also get durable writes for a fully local-first flow.
125+
That gives you a durable local base for a synced collection. Pair it with `@tanstack/offline-transactions`, and you also get durable writes for a local-first flow.
118126

119127
You can also use `persistedCollectionOptions(...)` without wrapping another synced collection config at all. In that mode, it is simply local state persisted to SQLite:
120128

@@ -131,7 +139,7 @@ const localDraftsCollection = createCollection(
131139

132140
`schemaVersion` is the switch that keeps those two modes honest. For synced collections, changing it tells TanStack DB to clear the persisted local copy and re-sync from the server. For unsynced local-only collections, changing it throws and requires the application to migrate the data itself.
133141

134-
That same persistence story also opens the door to runtimes outside the UI. As you'll see later in [createEffect](#createeffect-reactive-side-effects-for-workflows-tools-and-agents), a persisted TanStack DB running in something like a Cloudflare Durable Object starts to look a lot like a state engine for workflows and agents.
142+
That same persistence story also opens the door to runtimes outside the UI. As you'll see later in [createEffect](#createeffect-reactive-side-effects-for-workflows-tools-and-agents), a persistent TanStack DB running in something like a Cloudflare Durable Object starts to look a lot like a state engine for workflows and agents.
135143

136144
### Why SQLite
137145

@@ -141,18 +149,18 @@ We considered a split design where the browser would use IndexedDB directly to a
141149

142150
Standardizing on one persistence engine keeps the design simpler and lets us carry the same persistence model into mobile, desktop, server, edge, and agent-style runtimes instead of inventing a different system for each one.
143151

144-
We also weighed the cost of the WASM bundle. In practice, if users are already syncing data to the user's device, the extra cost of shipping SQLite WASM is relatively small. They are already pulling down meaningful application data, so paying a bit more upfront for a much cleaner persistence and query model felt like the right tradeoff.
152+
We also weighed the cost of the WASM bundle. In practice, if users are already syncing data to their devices, the extra cost of shipping SQLite WASM is relatively small. They are already pulling down meaningful application data, so paying a bit more upfront for a much cleaner persistence and query model feels like the right tradeoff.
145153

146154
### Why this matters
147155

148156
In practice, 0.6 gives you:
149157

150-
- apps can restart warm instead of cold
158+
- fast restarts for your apps
151159
- local state, both synced and pending mutations, can survive reloads and app restarts
152160
- offline-friendly UX becomes much more practical
153-
- the same DB mental model can move between mobile, browser, desktop, server, edge, and agent runtimes
161+
- the same DB mental model applies to all runtimes: mobile, browser, desktop, server, edge, and even AI agents
154162

155-
This is the first _alpha_ release of persistence, and so we are looking for feedback and testing - we want to hear your feedback.
163+
This is the first _alpha_ release of persistence, and so we want to hear your feedback.
156164

157165
## Includes: project your data into the same shape as your UI
158166

@@ -162,7 +170,7 @@ But most data systems make you choose between flat relational queries that you t
162170

163171
GraphQL tackles a similar problem from the server side: give the UI a hierarchical shape without forcing every client to manually stitch flat records back together.
164172

165-
`includes` is TanStack DB's answer to that same problem from the client side. It lets you retrieve normalized data and project it directly into the hierarchical structure your UI wants to render, over any data source TanStack DB can sit on top of, without needing GraphQL-specific infrastructure.
173+
`includes` is TanStack DB's answer to that same problem from the client side. It lets you retrieve normalized data and project it directly into the hierarchical structure rendered by your UI, over any TanStack DB data source, without needing GraphQL-specific infrastructure.
166174

167175
Instead of flattening `projects`, `issues`, and `comments` into repeated rows and rebuilding the tree yourself, you can express the hierarchy directly in the query:
168176

@@ -179,12 +187,19 @@ const projectsWithIssues = createLiveQueryCollection((q) =>
179187
.select(({ i }) => ({
180188
id: i.id,
181189
title: i.title,
190+
comments: q
191+
.from({ c: commentsCollection })
192+
.where(({ c }) => eq(c.issueId, i.id))
193+
.select(({ c }) => ({
194+
id: c.id,
195+
body: c.body,
196+
})),
182197
})),
183198
})),
184199
)
185200
```
186201

187-
The query above fetches all projects and, for each one, includes its issues by means of a nested query on the issues collection. The result is a collection of `{ id, name, issues }` objects where the issues themselves are also collections.
202+
The query above fetches all projects and, for each one, includes its issues and each issue's comments through nested sub-queries. The result is a collection of `{ id, name, issues }` objects where the nested fields are also collections.
188203

189204
### Why this is different
190205

@@ -195,7 +210,7 @@ The key thing here is that the whole nested query is executed as **one increment
195210
- if the engine has to go back to the server for multiple rows of an include, it does that once, not once per row
196211
- it keeps the same fine-grained incremental update model as the rest of TanStack DB
197212

198-
So this is not just a nicer projection API. It is also a performance and systems story.
213+
So this is not just a nicer projection API. It is also a performance and systems improvement.
199214

200215
### Fine-grained reactivity by default
201216

@@ -243,9 +258,9 @@ function IssueList({ issuesCollection }) {
243258
}
244259
```
245260

246-
### `toArray()` when you want materialised projections
261+
### `toArray()` when you want materialized projections
247262

248-
Sometimes you do not want a child collection. For simple aggregates, short lists like tags, or other places where you do not want a child render boundary, `toArray()` lets you materialize the child query directly in the projection layer.
263+
Sometimes you do not want a child collection. For simple aggregates, short lists like tags, or other places where it's better to avoid a child render boundary, `toArray()` lets you materialize the child query directly in the projection layer.
249264

250265
```typescript
251266
import { createLiveQueryCollection, eq, toArray } from '@tanstack/db'
@@ -273,12 +288,11 @@ With `toArray()`, the parent row is re-emitted when the child data changes. With
273288

274289
Includes in 0.6 support:
275290

276-
- nested child collections by default
291+
- arbitrarily nested subqueries with nested child collections by default
277292
- `toArray()` when you want materialized arrays instead
278293
- aggregates in child subqueries
279294
- `orderBy()` and `limit()` inside subqueries
280295
- child subqueries that filter based on their parent row
281-
- arbitrarily nested subqueries
282296
- usage patterns that preserve fine-grained updates at each level across all supported frameworks
283297

284298
Taken together, this is one of the biggest features in the release. It makes TanStack DB more suitable for building application-shaped views over normalized data.
@@ -287,7 +301,7 @@ Taken together, this is one of the biggest features in the release. It makes Tan
287301

288302
`createEffect` adds a reactive side-effect layer on top of live queries.
289303

290-
You can think of it a little bit like a database trigger, except it runs on the result of an arbitrary live query instead of only on writes to a single table. That means you can define side effects from the shape of the data you care about, not just from raw mutations at the storage layer.
304+
You can think of it like a database trigger, except it runs on the result of an arbitrary live query instead of only on writes to a single table. That means you can define side effects from the shape of the data you care about, not just from raw mutations at the storage layer.
291305

292306
Effects also do **not** materialize the full result of the query into a collection first. They run incrementally on query-result deltas, which keeps them low-memory and makes them a much better fit for workflow logic than "subscribe to a whole collection and diff it yourself", especially because the query engine itself is already incremental.
293307

@@ -320,7 +334,7 @@ const effect = createEffect({
320334
await effect.dispose()
321335
```
322336

323-
Combined with [persisted local state](#persisted-local-state) in something like a Cloudflare Durable Object, TanStack DB starts to look like a durable state engine for agent workflows, not just a UI data layer. This is only one example, but it shows why the 0.6 features matter together: [includes](#includes-project-your-data-into-the-same-shape-as-your-ui), [virtual props](#virtual-props-outboxes-delivery-state-and-row-provenance), and reactive effects all compose into something much more powerful than any one feature on its own.
337+
Combined with [persistent local state](#persistent-local-state) in something like a Cloudflare Durable Object, TanStack DB starts to look like a durable state engine for agent workflows, not just a UI data layer. This is only one example, but it shows why the 0.6 features matter together: [includes](#includes-project-your-data-into-the-same-shape-as-your-ui), [virtual props](#virtual-props-outboxes-delivery-state-and-row-provenance), and reactive effects all compose into something much more powerful than any one feature on its own.
324338

325339
## Virtual props: outboxes, delivery state, and row provenance
326340

@@ -333,11 +347,11 @@ They are:
333347
- `$key`: the row key for the result
334348
- `$collectionId`: the source collection ID
335349

336-
That gives you access to state that used to be awkward or bolted on.
350+
That gives you access to state that used to be hidden.
337351

338352
You can use them for workflow automation together with `createEffect`, but they are also immediately useful for UI:
339353

340-
- an outbox view of un-persisted data
354+
- an outbox view of unpersisted data
341355
- a delivery or sync state badge
342356
- the little double-tick style UI we are used to from apps like WhatsApp
343357

@@ -375,7 +389,7 @@ Not every query needs to stay live.
375389
- tests
376390
- AI and LLM context building
377391

378-
It is a small feature, but it rounds out the API in an important way. You can now use the same query language for both reactive and one-off reads.
392+
It is a small feature, but it completes the API in an important way. You can now use the same query language for both reactive and one-off reads.
379393

380394
```typescript
381395
import { eq, queryOnce } from '@tanstack/db'
@@ -437,7 +451,7 @@ const collection = createCollection({
437451

438452
### Magic return removal
439453

440-
We are also removing the "magic return" behavior from mutation handlers in favor of the more explicit and uniform model. The explicit options were already there. They are not new in 0.6. What is changing is that we are standardizing on one clear way to do it.
454+
We are also removing the "magic return" behavior from mutation handlers in favor of a more explicit and uniform model. The explicit options were already there. They are not new in 0.6. What is changing is that we are standardizing on one clear way to do it.
441455

442456
The important rule is simple:
443457

@@ -479,9 +493,9 @@ But there is still one major missing piece on the path to v1: **server-side rend
479493

480494
TanStack DB is different from TanStack Query and from a classic API-driven application architecture. The SSR story is not just "do what Query does, but for DB". DB has a different execution model, a different relationship between local and remote state, and a different set of tradeoffs around hydration, persistence, and live updates.
481495

482-
So rather than rushing into a shallow solution, we want design partners. We are actively exploring the shape of SSR support for TanStack DB, and we want to hear from teams who are interested in using it seriously.
496+
So rather than rushing into a shallow solution, we want to think this through with design partners. We are actively exploring the shape of SSR support for TanStack DB, and we want to hear from teams interested in using it seriously.
483497

484-
If that is you, please fill out the design partner form and tell us about your app, your constraints, and what a good SSR story for DB would need to look like. We will set up calls with teams, interview them to understand the requirements, and run proposals past them as we shape the design.
498+
If that is you, please fill out the design partner form and tell us about your app, your constraints, and what a good SSR story for DB would need to look like. We will set up calls with teams, interview them to understand the requirements and run proposals past them as we shape the design.
485499

486500
- [Fill out the SSR design partner form](https://docs.google.com/forms/d/e/1FAIpQLSdoCZ_Z5uODArGpGkVI4tbU7q9qHAcGAXYYEoP9HFq3aKNs3A/viewform?usp=publish-editor).
487501

0 commit comments

Comments
 (0)