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;-- Automatically update the updated_at timestampCREATE TRIGGER _todos__update_trigger AFTER UPDATE ON todos FOR EACH ROW BEGIN UPDATE todos SET updated_at = UNIXEPOCH() WHERE id = OLD.id; END;
Key Requirements:
Tables must use STRICT typing
Primary key must be INTEGER or BLOB (for UUIDv7)
Column names should be quoted to avoid SQL keyword conflicts
Restart TrailBase to apply the migration:
# Stop with Ctrl+C, then restarttrail run
You’ll see a log message confirming the migration was applied.
Subscribe to live changes to sync todos across tabs:
src/App.tsx
import { initClient, type Event } from "trailbase";// Add to your componentuseEffect(() => { let reader: ReadableStreamDefaultReader<Event> | null = null; async function subscribe() { try { // Subscribe to all todos const stream = await todosApi.subscribeAll(); reader = stream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; // Handle different event types if ("Insert" in value || "Update" in value || "Delete" in value) { // Reload todos when changes occur loadTodos(); } } } catch (err) { console.error("Subscription error:", err); } } subscribe(); return () => { reader?.cancel(); };}, []);
For production apps, use a debounce mechanism to avoid excessive reloads when handling rapid updates.
Update your API configuration to require authentication:
traildepot/config.textproto
record_apis: [ { name: "todos" table_name: "todos" # Only authenticated users can access acl_authenticated: [CREATE, READ, UPDATE, DELETE] }]
Add a user column to track todo ownership:
trail migration add_user_to_todos
-- Add user columnALTER TABLE todos ADD COLUMN user BLOB REFERENCES _user(id) ON DELETE CASCADE;-- Create index for faster lookupsCREATE INDEX _todos__user_index ON todos(user);
Update the API to auto-fill the user column:
traildepot/config.textproto
record_apis: [ { name: "todos" table_name: "todos" autofill_missing_user_id_columns: true acl_authenticated: [CREATE, READ, UPDATE, DELETE] # Users can only see their own todos read_access_rule: "_ROW_.user = _USER_.id" # Users can only modify their own todos update_access_rule: "_ROW_.user = _USER_.id" delete_access_rule: "_ROW_.user = _USER_.id" }]
Add authentication to your frontend:
src/App.tsx
import { initClient } from "trailbase";const client = initClient("http://localhost:4000");function App() { const [user, setUser] = useState(client.user()); async function login(email: string, password: string) { const response = await client.login({ email, password }); setUser(client.user()); } async function logout() { await client.logout(); setUser(null); } if (!user) { return <LoginForm onLogin={login} />; } return ( <div> <button onClick={logout}>Logout ({user.email})</button> {/* Your todo list component */} </div> );}