Documentation Index
Fetch the complete documentation index at: https://docs.cuttlefishsync.com/llms.txt
Use this file to discover all available pages before exploring further.
Concepts
Understand how Cuttlefish works and what makes it powerful.
Live SQL
When you write useLiveQuery("SELECT * FROM todos"), you’re writing Live SQL - standard SQL queries that automatically update your UI when data changes.
const { data } = useLiveQuery(sql`SELECT * FROM todos WHERE completed = false`)
There’s no new query language to learn. If you know SQL, you know how to use Cuttlefish. The queries are live - when the underlying data changes in your database, your component re-renders with the new data automatically.
What’s happening: Live Partial Replica
Behind the scenes, Cuttlefish builds a Live Partial Replica of your database in the browser.
When you run a live query:
- Cuttlefish Engine (server) executes your SQL against Postgres
- Initial data is sent to the browser and stored in RowCache (normalized, in-memory storage)
- Subsequent changes stream to the browser in real-time via WebSocket
- QueryEvaluator runs your query against the local RowCache
- React re-renders with fresh data
Your UI is always in sync because the local replica updates automatically.
Why it’s powerful: Normalized Replication
Here’s what makes Cuttlefish different from other real-time solutions: it sends normalized relations, not denormalized projections.
Example: Messages with user names
Most real-time tools:
// Query: SELECT users.name, messages.text FROM messages JOIN users
// Returns denormalized data:
[
{ name: "Alice", text: "Hello" },
{ name: "Alice", text: "How are you?" },
{ name: "Bob", text: "I'm good!" }
]
Cuttlefish:
// Same query, but returns normalized relations:
{
users: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
],
messages: [
{ id: 1, user_id: 1, text: "Hello" },
{ id: 2, user_id: 1, text: "How are you?" },
{ id: 3, user_id: 2, text: "I'm good!" }
]
}
Why does this matter?
With denormalized data, if you want to filter messages differently (e.g., by read status), you need a new server query - the data isn’t structured for it.
With Cuttlefish’s normalized RowCache, the data is already there. Filter by status, user, date - all instant. No new server queries. This is what enables the Composable Sync pattern.
Unlocking more: Composable Sync
The normalized replica enables a powerful pattern: Composable Sync - “replicate once, query infinitely.”
Two ways to query
Pattern 1: The convenient way
const { data } = useLiveQuery(sql`SELECT * FROM todos WHERE list_id = ${listId}`)
This replicates and queries in one step. Simple and perfect for most cases.
Pattern 2: The composable primitives
// Replicate once (on app load or route mount)
replicateQuery(sql`SELECT * FROM todos`)
// Query infinitely - runs locally, zero latency
const { data } = useLocalLiveQuery(sql`SELECT * FROM todos WHERE list_id = ${listId}`)
With the composable pattern, data is already in your local RowCache. Changing filters, sorting, pagination - all instant. No loading states, no server round-trips.
Zero loading states everywhere
Once you’ve replicated data, every interaction is instant:
Filtering and search
replicateQuery(sql`SELECT * FROM issues`)
// User toggles filter - instant results from RowCache
const { data } = useLocalLiveQuery(
sql`SELECT * FROM issues WHERE status = ${filterValue}`
)
Page navigation
replicateQuery(sql`SELECT * FROM issues`)
// Navigate to project page - instant, no loading spinner
const { data } = useLocalLiveQuery(
sql`SELECT * FROM issues WHERE project_id = ${projectId}`
)
Sorting
replicateQuery(sql`SELECT * FROM issues`)
// Click column header - instant reorder
const { data } = useLocalLiveQuery(
sql`SELECT * FROM issues ORDER BY ${sortColumn} ${sortDirection}`
)
Switching views
replicateQuery(sql`SELECT * FROM issues`)
// "My Issues" → "Team Issues" → "All Issues" - all instant
const { data: myIssues } = useLocalLiveQuery(
sql`SELECT * FROM issues WHERE assignee_id = ${userId}`
)
const { data: teamIssues } = useLocalLiveQuery(
sql`SELECT * FROM issues WHERE team_id = ${teamId}`
)
The data is already local. Every query is just a different view into the same RowCache.
The pieces: Engine and Client
Cuttlefish is built from two pieces that work together:
Cuttlefish Engine (server)
- Executes SQL against Postgres
- Performs normalized replication
- Streams changes via WebSocket
- Language-agnostic - use from Python, Go, Ruby, or any language
Client packages (browser)
@cuttlefish-sync/core - RowCache, QueryEvaluator, protocol types
@cuttlefish-sync/react - React hooks (useLiveQuery, useLocalLiveQuery, replicateQuery)
@cuttlefish-sync/kysely - Kysely adapter for type-safe queries
@cuttlefish-sync/raw-sql - Raw SQL tagged template adapter
The React client is optimized for the browser, but the core architecture (RowCache + QueryEvaluator) is runtime-agnostic and works in any JavaScript environment.