Skip to content

Live Queries

Live queries allow the server to push updates to clients whenever the underlying data changes, using resource-based invalidation and JSON diff-patching.

  1. A query is marked as @live in the schema
  2. The server tracks which resource identifiers the query accesses
  3. When a mutation invalidates one of those identifiers, the query re-executes
  4. Only the diff is sent to the client via jsondiffpatch

Add the @live directive to query fields in your schema:

type Query {
users: [User!]! @live
}

From within a resolver, use the liveQueryStore from context:

const resolvers = {
Mutation: {
createUser: async (_, args, context) => {
const user = await db.createUser(args)
await context.liveQueryStore.invalidate('Query.users')
return user
},
},
}

The invalidate function accepts multiple formats:

// Simple string
liveQueryStore.invalidate('Query.users')
// Multiple identifiers
liveQueryStore.invalidate(['Query.users', 'Query.userCount'])
// Builder function
liveQueryStore.invalidate((tools) => [
tools.id('User', user.id),
tools.args('Query.user', { id: user.id }),
])

By default, when a @live query has arguments, the system creates an identifier that includes all arguments. This means invalidating the query requires knowing the exact argument combination each client used.

This becomes a problem with filtered lists — for example, a tasks query with a filter argument. Every unique filter combination produces a different identifier, making it impractical to invalidate all active tasks queries when a task is created:

# ❌ Without @liveIdentifier, each filter combination is a separate identifier.
# Invalidating "Query.tasks" won't reach clients using "Query.tasks(filter: ...)"
type Query {
tasks(filter: TaskFilter): [Task!]! @live
}

Use @liveIdentifier to explicitly select which arguments should be part of the identifier. Arguments not marked with @liveIdentifier are excluded, so the filter no longer splits subscribers into separate identifiers:

# ✅ Only 'projectId' scopes the identifier — filter is ignored
type Query {
tasks(projectId: ID! @liveIdentifier, filter: TaskFilter): [Task!]! @live
}

To invalidate all tasks for a given project, regardless of filter:

liveQueryStore.invalidate((t) => t.args('Query.tasks', { projectId: 'xyz' }))

When NODE_ENV is not set to production, the server includes the generated live query identifiers in the response extensions. You can inspect them in your browser’s devtools under the Socket (or WS) tab — each live query response will contain the resource identifiers it subscribed to. This is useful for verifying that your @liveIdentifier directives produce the expected identifiers.