{"slug":"self-hosted-live-queries-react-tako-stores","url":"https://tako.sh/blog/self-hosted-live-queries-react-tako-stores/","canonical":"https://tako.sh/blog/self-hosted-live-queries-react-tako-stores/","title":"Self-Hosted Live Queries for React: Introducing Tako Stores","date":"2026-10-25T04:17","description":"Keep React synchronized with your existing database using Tako Stores: authenticated live reads, explicit invalidation, and optional offline caching.","author":null,"image":"62991c3db48d","imageAlt":null,"headings":[{"depth":2,"slug":"your-database-an-authorized-loader-a-react-hook","text":"Your database, an authorized loader, a React hook"},{"depth":2,"slug":"commit-the-write-then-announce-the-change","text":"Commit the write, then announce the change"},{"depth":2,"slug":"let-saved-reads-survive-a-lost-connection","text":"Let saved reads survive a lost connection"}],"markdown":"You save a message. The database has it. The person in the next browser tab is still looking at yesterday's conversation. Somewhere between “write succeeded” and “everyone can see it,” you need subscriptions, permission checks, reconnect handling, and a way to ask for fresh data.\n\nWe're introducing [Tako Stores](/docs/stores/): self-hosted live queries for React, backed by the database you already use. Define an authenticated loader, read it with `useStore`, and invalidate it after a write. Tako connects those pieces and reloads the view when notified. You can also opt into cached reads for when the connection disappears.\n\n“Live” has a specific meaning here: your application announces changes explicitly. Stores don't inspect SQL dependencies or automatically observe database writes. That keeps the integration in your application code, where you already know what changed and who may read it.\n\n## Your database, an authorized loader, a React hook\n\nA Store starts with a backend definition under `<app_root>/stores/`. For an app whose root is `src`, a chat's message list could look like this:\n\n```ts\n// src/stores/messages.ts\nimport { defineStore } from \"tako.sh\";\nimport { db } from \"../lib/db\";\nimport { verifyToken, canAccessChat } from \"../lib/auth\";\n\nexport default defineStore(\"messages\", {\n  paramsSchema: (t) => t.Object({ chatId: t.String({ minLength: 1 }) }),\n  auth: {\n    async verify({ header, params }) {\n      const user = await verifyToken(header?.value);\n      if (!user || !(await canAccessChat(user.id, params.chatId))) return false;\n      return { subject: user.id };\n    },\n  },\n  async load({ params }) {\n    const rows = await db.message.findMany({\n      where: { chatId: params.chatId },\n      orderBy: { createdAt: \"asc\" },\n    });\n    return rows.map((row) => ({ id: row.id, text: row.text }));\n  },\n});\n```\n\nThe database and authentication helpers belong to your app. Here, `verifyToken` validates the authorization header and `canAccessChat` checks membership. Replace those helpers and the query with your existing code. The loader can read from a database or another service; it returns the view your UI needs.\n\nTako validates params and checks authorization independently for reads and subscriptions. Returning a trusted `subject` also makes that identity available to `load`, so a loader can apply per-reader filtering. Each authorized reader gets a separately loaded snapshot. The subscription never broadcasts those records to everyone watching the same chat.\n\nThis example deliberately configures authentication. Omit `auth`, and the Store is public. Cookie authentication is available too; the [Stores reference](/docs/stores/) covers cookie-only and custom-header setups.\n\nAfter adding the definition, run [`tako generate`](/docs/cli/) to refresh the generated Store types. React imports the hook, without importing backend code:\n\n```tsx\n\"use client\";\n\nimport { useStore } from \"tako.sh/react\";\n\nexport function Messages({ chatId, token }: { chatId: string; token: string }) {\n  const { data, status, isStale, error, refresh } = useStore(\"messages\", {\n    params: { chatId },\n    authorization: token,\n  });\n\n  if (data === undefined) {\n    return <p>{error ? \"Messages are unavailable.\" : \"Loading messages…\"}</p>;\n  }\n\n  return (\n    <>\n      {isStale && <p>Messages may be out of date; waiting for an update.</p>}\n      <ul>\n        {data.map((message) => (\n          <li key={message.id}>{message.text}</li>\n        ))}\n      </ul>\n      <button disabled={status === \"loading\"} onClick={() => void refresh()}>\n        Refresh\n      </button>\n    </>\n  );\n}\n```\n\nGenerated types connect the Store name to its params and serialized result. Returning explicit response objects keeps that shape easy to understand. The hook exposes the latest data, connection-related status, staleness, errors, and manual refresh. Your component decides how those states should feel to a reader.\n\nEach hook owns its subscription. If several components need the same view, put the hook in a provider and share its result. JavaScript isn't required on the backend either: Go and Rust have Store registration APIs that work with the same browser client and React hook.\n\n## Commit the write, then announce the change\n\nThe write path stays in your existing handler. Once its database transaction has committed, invalidate the Store with the affected params:\n\n```ts\nimport messages from \"../stores/messages\";\n\nawait db.message.create({ data: { chatId, text } });\nawait messages.invalidate({ chatId });\n```\n\nThat wakes readers of `messages` with those exact params. Each read goes through authorization again and loads current data. Invalidating one chat doesn't refresh another, and there is no wildcard invalidation. An unparameterized Store uses `invalidate()`.\n\nUnderneath, Stores use [Tako Channels](/docs/channels/) for notifications over an SSE subscription. The browser receives a small invalidation signal, then requests a fresh snapshot:\n\n```d2\ndirection: right\n\nwrite: \"App write\" {style.fill: \"#FFF9F4\"}\ndb: \"Your database\" {shape: cylinder; style.fill: \"#9BC4B6\"}\nstore: \"Tako Store\" {style.fill: \"#E88783\"}\nreact: \"React reader\" {style.fill: \"#9BC4B6\"}\n\nwrite -> db: \"1. commit\"\nwrite -> store: \"2. invalidate(params)\"\nstore -> react: \"3. notify\"\nreact -> store: \"4. authorized read\"\nstore -> db: \"5. load current data\"\nstore -> react: \"6. private snapshot\"\n```\n\nThe notification contains no records, credentials, or raw params. Keeping data in the read path means two people subscribed to the same params can still receive different results according to their permissions and loader logic.\n\nReconnects matter just as much as the happy path. The client reloads after its stream is established and after it reconnects. If another invalidation arrives during a load, it follows up with another read. Several notifications can be coalesced: the goal is a current view, so the client doesn't promise to replay every intermediate database state.\n\nThere is an application responsibility here. Every writer that should update an open UI needs to invalidate the affected Store, whether that writer is a route, a workflow, or a separate script. A direct database edit won't automatically produce a notification.\n\nThe database commit and notification publication are also separate operations. If publication fails, `invalidate` reports the failure; the committed write is neither rolled back nor retried by Stores. Where eventual notification must survive that gap, use a transactional application outbox: record the notification intent alongside the write, then publish it from your delivery process. Reconnect catch-up and manual refresh can fetch current data, but they don't make those two operations atomic.\n\n## Let saved reads survive a lost connection\n\nA connection dropping shouldn't always mean an empty screen. For data appropriate to keep on a device, enable offline persistence on both sides. Add this policy to the backend definition:\n\n```ts\noffline: { maxAgeMs: 60 * 60 * 1000, version: \"1\" },\n```\n\nThen add a stable, non-secret identity scope to the hook options:\n\n```ts\nconst { data, status, isStale } = useStore(\"messages\", {\n  params: { chatId },\n  authorization: token,\n  cacheScope: `${tenantId}:${userId}`,\n  cacheVersion: \"1\",\n});\n```\n\nBoth choices are required. Without a backend policy or a client scope, results aren't persisted. The default browser cache uses IndexedDB and separates entries by origin, Store, params, scope, and version. Authorization credentials aren't stored. Change the scope when switching accounts or tenants, and bump both versions after an incompatible response-shape change.\n\n| Situation                                      | What the reader can expect                              |\n| ---------------------------------------------- | ------------------------------------------------------- |\n| Connection lost, valid cached result available | Previously loaded data can remain visible, marked stale |\n| Cached result expired or version mismatched    | That cached result is ignored                           |\n| Read or subscription denied authorization      | The active result and its persisted entry are cleared   |\n| Browser storage unavailable                    | Online reads still work                                 |\n\nClear the matching scope during logout and disable or unmount that session's readers:\n\n```ts\nimport { clearStoreCache } from \"tako.sh/client\";\n\nawait clearStoreCache(`${tenantId}:${userId}`);\n```\n\nOffline reads are previously authorized data saved on that device. A disconnected browser cannot discover a remote permission revocation, so choose the data and expiry accordingly. The app shell must also already be available: Stores doesn't install a service worker. This release provides cached reads; queued writes, optimistic mutations, and conflict resolution aren't included.\n\nYou can develop this flow with [`tako dev`](/docs/development/), which watches JavaScript Store definitions and enforces Store authorization locally. Notification state is in memory during development and uses Tako-managed SQLite by default on a single production server. For multiple servers, configure the shared `postgres_url` credential so notifications can reach readers connected elsewhere. Your application database remains the source of truth.\n\nStart with one view that's frustrating to keep fresh. Define its loader, connect the hook, and invalidate after its writes. The [Stores documentation](/docs/stores/) has the complete JavaScript, Go, and Rust examples; the [implementation is on GitHub](https://github.com/tako-sh/tako). Your database already knows what's new. Now your React UI has a way to find out."}