Skip to main content

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:
  1. Cuttlefish Engine (server) executes your SQL against Postgres
  2. Initial data is sent to the browser and stored in RowCache (normalized, in-memory storage)
  3. Subsequent changes stream to the browser in real-time via WebSocket
  4. QueryEvaluator runs your query against the local RowCache
  5. 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:
replicateQuery(sql`SELECT * FROM issues`)

// User toggles filter - instant results from RowCache
const { data } = useLocalLiveQuery(
  sql`SELECT * FROM issues WHERE status = ${filterValue}`
)
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.