Skip to content

Add TrailBase integration and example #228

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/six-chefs-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@tanstack/trailbase-db-collection": patch
"@tanstack/electric-db-collection": patch
"@tanstack/query-db-collection": patch
"@tanstack/db-example-react-todo": patch
"@tanstack/react-db": patch
"@tanstack/vue-db": patch
"@tanstack/db": patch
---

Add initial release of TrailBase collection for TanStack DB. TrailBase is a blazingly fast, open-source alternative to Firebase built on Rust, SQLite, and V8. It provides type-safe REST and realtime APIs with sub-millisecond latencies, integrated authentication, and flexible access control - all in a single executable. This collection type enables seamless integration with TrailBase backends for high-performance real-time applications.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,22 @@ There's also an example [React todo app](./examples/react/todo) and usage exampl
- batch and stage local changes across collections with immediate application of local optimistic updates
- sync transactions to the backend with automatic rollbacks and management of optimistic state

## 📦 Collection Types

TanStack DB provides several collection types to support different backend integrations:

- **`@tanstack/db`** - Core collection functionality with local-only and local-storage collections for offline-first applications
- **`@tanstack/query-db-collection`** - Collections backed by [TanStack Query](https://tanstack.com/query) for REST APIs and GraphQL endpoints
- **`@tanstack/electric-db-collection`** - Real-time sync collections powered by [ElectricSQL](https://electric-sql.com) for live database synchronization
- **`@tanstack/trailbase-db-collection`** - Collections for [TrailBase](https://trailbase.io) backend integration

## Framework integrations

TanStack DB integrates with React & Vue with more on the way!

- **`@tanstack/react-db`** - React hooks and components for using TanStack DB collections in React applications
- **`@tanstack/vue-db`** - Vue composables for using TanStack DB collections in Vue applications

## 🔧 Install

```bash
Expand Down
7 changes: 5 additions & 2 deletions examples/react/todo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
- Install packages
`pnpm install`

- Start dev server & Docker containers
- Start dev server & Docker containers: Postgres, Electric, TrailBase
`pnpm dev`

- Run db migrations
- Run Postgres DB migrations
`pnpm db:push`

- Optionally, check out the TrailBase admin UI @ http://localhost:4000/\_/admin
(email: admin@localhost, password: secret)
9 changes: 9 additions & 0 deletions examples/react/todo/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,14 @@ services:
postgres:
condition: service_healthy

trailbase:
image: trailbase/trailbase:latest
ports:
- "${PORT:-4000}:4000"
restart: unless-stopped
volumes:
- ./traildepot:/app/traildepot
command: "/app/trail --data-dir /app/traildepot run --address 0.0.0.0:4000 --dev"

volumes:
postgres_data:
2 changes: 2 additions & 0 deletions examples/react/todo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@tanstack/react-db": "^0.0.25",
"@tanstack/react-router": "^1.125.6",
"@tanstack/react-start": "^1.126.1",
"@tanstack/trailbase-db-collection": "^0.0.1",
"cors": "^2.8.5",
"drizzle-orm": "^0.40.1",
"drizzle-zod": "^0.7.0",
Expand All @@ -17,6 +18,7 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.11",
"trailbase": "^0.7.1",
"vite-tsconfig-paths": "^5.1.4"
},
"devDependencies": {
Expand Down
5 changes: 5 additions & 0 deletions examples/react/todo/src/components/NotFound.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export function NotFound() {
Electric Demo
</button>
</Link>
<Link to="/trailbase" className="flex-1">
<button className="w-full px-4 py-2 bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors text-sm">
TrailBase Demo
</button>
</Link>
</div>
</div>
</div>
Expand Down
6 changes: 6 additions & 0 deletions examples/react/todo/src/components/TodoApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ export function TodoApp({
>
Electric
</Link>
<Link
to="/trailbase"
className="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 transition-colors"
>
TrailBase
</Link>
</div>
</div>

Expand Down
60 changes: 60 additions & 0 deletions examples/react/todo/src/lib/collections.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { createCollection } from "@tanstack/react-db"
import { electricCollectionOptions } from "@tanstack/electric-db-collection"
import { queryCollectionOptions } from "@tanstack/query-db-collection"
import { trailBaseCollectionOptions } from "@tanstack/trailbase-db-collection"
import { QueryClient } from "@tanstack/query-core"
import { initClient } from "trailbase"
import { selectConfigSchema, selectTodoSchema } from "../db/validation"
import { api } from "./api"
import type { SelectConfig, SelectTodo } from "../db/validation"

// Create a query client for query collections
const queryClient = new QueryClient()

// Create a TrailBase client.
const trailBaseClient = initClient(`http://localhost:4000`)

// Electric Todo Collection
export const electricTodoCollection = createCollection(
electricCollectionOptions({
Expand Down Expand Up @@ -101,6 +107,33 @@ export const queryTodoCollection = createCollection(
})
)

type Todo = {
id: number
text: string
completed: boolean
created_at: number
updated_at: number
}

// TrailBase Todo Collection
export const trailBaseTodoCollection = createCollection(
trailBaseCollectionOptions<SelectTodo, Todo>({
id: `todos`,
getKey: (item) => item.id,
schema: selectTodoSchema,
recordApi: trailBaseClient.records(`todos`),
// Re-using the example's drizzle-schema requires remapping the items.
parse: {
created_at: (ts) => new Date(ts * 1000),
updated_at: (ts) => new Date(ts * 1000),
},
serialize: {
created_at: (date) => Math.floor(date.valueOf() / 1000),
updated_at: (date) => Math.floor(date.valueOf() / 1000),
},
})
)

// Electric Config Collection
export const electricConfigCollection = createCollection(
electricCollectionOptions({
Expand Down Expand Up @@ -168,3 +201,30 @@ export const queryConfigCollection = createCollection(
},
})
)

type Config = {
id: number
key: string
value: string
created_at: number
updated_at: number
}

// TrailBase Config Collection
export const trailBaseConfigCollection = createCollection(
trailBaseCollectionOptions<SelectConfig, Config>({
id: `config`,
getKey: (item) => item.id,
schema: selectConfigSchema,
recordApi: trailBaseClient.records(`config`),
// Re-using the example's drizzle-schema requires remapping the items.
parse: {
created_at: (ts) => new Date(ts * 1000),
updated_at: (ts) => new Date(ts * 1000),
},
serialize: {
created_at: (date) => Math.floor(date.valueOf() / 1000),
updated_at: (date) => Math.floor(date.valueOf() / 1000),
},
})
)
24 changes: 21 additions & 3 deletions examples/react/todo/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { createServerRootRoute } from '@tanstack/react-start/server'

import { Route as rootRouteImport } from './routes/__root'
import { Route as TrailbaseRouteImport } from './routes/trailbase'
import { Route as QueryRouteImport } from './routes/query'
import { Route as ElectricRouteImport } from './routes/electric'
import { Route as IndexRouteImport } from './routes/index'
Expand All @@ -21,6 +22,11 @@ import { ServerRoute as ApiConfigIdServerRouteImport } from './routes/api/config

const rootServerRouteImport = createServerRootRoute()

const TrailbaseRoute = TrailbaseRouteImport.update({
id: '/trailbase',
path: '/trailbase',
getParentRoute: () => rootRouteImport,
} as any)
const QueryRoute = QueryRouteImport.update({
id: '/query',
path: '/query',
Expand Down Expand Up @@ -61,30 +67,34 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/electric': typeof ElectricRoute
'/query': typeof QueryRoute
'/trailbase': typeof TrailbaseRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/electric': typeof ElectricRoute
'/query': typeof QueryRoute
'/trailbase': typeof TrailbaseRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/electric': typeof ElectricRoute
'/query': typeof QueryRoute
'/trailbase': typeof TrailbaseRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/electric' | '/query'
fullPaths: '/' | '/electric' | '/query' | '/trailbase'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/electric' | '/query'
id: '__root__' | '/' | '/electric' | '/query'
to: '/' | '/electric' | '/query' | '/trailbase'
id: '__root__' | '/' | '/electric' | '/query' | '/trailbase'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ElectricRoute: typeof ElectricRoute
QueryRoute: typeof QueryRoute
TrailbaseRoute: typeof TrailbaseRoute
}
export interface FileServerRoutesByFullPath {
'/api/config': typeof ApiConfigServerRouteWithChildren
Expand Down Expand Up @@ -125,6 +135,13 @@ export interface RootServerRouteChildren {

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/trailbase': {
id: '/trailbase'
path: '/trailbase'
fullPath: '/trailbase'
preLoaderRoute: typeof TrailbaseRouteImport
parentRoute: typeof rootRouteImport
}
'/query': {
id: '/query'
path: '/query'
Expand Down Expand Up @@ -209,6 +226,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ElectricRoute: ElectricRoute,
QueryRoute: QueryRoute,
TrailbaseRoute: TrailbaseRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
Expand Down
2 changes: 1 addition & 1 deletion examples/react/todo/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const Route = createRootRoute({
content: `width=device-width, initial-scale=1`,
},
{
title: `TanStack Start/DB/Electric Starter`,
title: `TanStack DB Example`,
},
],
links: [
Expand Down
8 changes: 8 additions & 0 deletions examples/react/todo/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ function HomePage() {
</div>
</button>
</Link>
<Link to="/trailbase" className="block w-full">
<button className="w-full px-6 py-4 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-left">
<div className="font-semibold">TrailBase Collections</div>
<div className="text-sm opacity-90 mt-1">
Real-time sync with TrailBase
</div>
</button>
</Link>
</div>

<div className="mt-8 text-xs text-center text-gray-500">
Expand Down
43 changes: 43 additions & 0 deletions examples/react/todo/src/routes/trailbase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createFileRoute } from "@tanstack/react-router"
import { useLiveQuery } from "@tanstack/react-db"
import {
trailBaseConfigCollection,
trailBaseTodoCollection,
} from "../lib/collections"
import { TodoApp } from "../components/TodoApp"

export const Route = createFileRoute(`/trailbase`)({
component: TrailBasePage,
ssr: false,
loader: async () => {
await Promise.all([
trailBaseTodoCollection.preload(),
trailBaseConfigCollection.preload(),
])

return null
},
})

function TrailBasePage() {
// Get data using live queries with Electric collections
const { data: todos } = useLiveQuery((q) =>
q
.from({ todo: trailBaseTodoCollection })
.orderBy(({ todo }) => todo.created_at, `asc`)
)

const { data: configData } = useLiveQuery((q) =>
q.from({ config: trailBaseConfigCollection })
)

return (
<TodoApp
todos={todos}
configData={configData}
todoCollection={trailBaseTodoCollection}
configCollection={trailBaseConfigCollection}
title="todos (TrailBase)"
/>
)
}
8 changes: 8 additions & 0 deletions examples/react/todo/traildepot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Deployment-specific directories:
backups/
data/
secrets/
uploads/

trailbase.js
trailbase.d.ts
24 changes: 24 additions & 0 deletions examples/react/todo/traildepot/config.textproto
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
email {}
server {
application_name: "TanStack-DB TrailBase Example"
logs_retention_sec: 604800
}
auth {
auth_token_ttl_sec: 3600
refresh_token_ttl_sec: 2592000
}
jobs {}
record_apis: [
{
name: "todos"
table_name: "todos"
acl_world: [CREATE, READ, UPDATE, DELETE]
enable_subscriptions: true
},
{
name: "config"
table_name: "config"
acl_world: [CREATE, READ, UPDATE, DELETE]
enable_subscriptions: true
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Create default admin user with top "secret" password.
INSERT INTO _user
(email, password_hash, verified, admin)
VALUES
('admin@localhost', (hash_password('secret')), TRUE, TRUE);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE todos (
"id" INTEGER PRIMARY KEY NOT NULL,
"text" TEXT NOT NULL,
"completed" INTEGER NOT NULL DEFAULT 0,
"created_at" INTEGER NOT NULL DEFAULT(UNIXEPOCH()),
"updated_at" INTEGER NOT NULL DEFAULT(UNIXEPOCH())
) STRICT;

CREATE TRIGGER _todos__update_trigger AFTER UPDATE ON todos FOR EACH ROW
BEGIN
UPDATE todos SET updated_at = UNIXEPOCH() WHERE id = OLD.id;
END;
Loading