Igniter.js Realtime: Live Data, Effortlessly
Igniter.js Realtime is the framework's integrated solution for pushing live data from the server to connected clients. Built on top of the robust and simple Server-Sent Events (SSE) web standard, it allows you to build real-time features like live UI updates, notifications, and activity feeds with minimal effort and maximum type safety.
The Realtime system is designed around two primary use cases:
- Automatic UI Revalidation: The most powerful feature. Automatically refetch data on your clients after a mutation on the server, ensuring your UI is always in sync with your backend state.
- Custom Data Streams: Create dedicated, real-time channels for features like notifications, chat messages, or live data dashboards.
How It Works: The SSE Connection
Under the hood, when you enable realtime features on the client, the IgniterProvider
establishes a single, persistent SSE connection to a dedicated endpoint on your Igniter.js server.
- Client-Side: The client subscribes to specific "channels" over this single connection. These channels can be for revalidation events or for custom data streams.
- Server-Side: When an event occurs (e.g., a database record is updated), the server publishes a message to the relevant channel. All clients subscribed to that channel will receive the message instantly.
This approach is highly efficient as it uses a single long-lived connection per client, avoiding the overhead of WebSockets for scenarios where only server-to-client communication is needed.
1. Automatic UI Updates with .revalidate()
This is the "magic" of Igniter.js Realtime. You can trigger a client-side data refetch directly from your backend mutation, ensuring that any user viewing that data sees the update instantly.
Step 1: The Mutation (Backend)
In your mutation
handler, after you've successfully modified data, chain the .revalidate()
method to your response.
.revalidate()
takes one argument: an array of query keys to invalidate. The query key is typically the path to the query action on your client-side api
object (e.g., api.users.list
becomes 'users.list'
).
// src/features/posts/controllers/post.controller.ts
import { igniter } from '@/igniter';
import { z } from 'zod';
export const postController = igniter.controller({
path: '/posts',
actions: {
// A query to list all posts
list: igniter.query({
path: '/',
handler: async ({ context, response }) => {
const posts = await context.database.post.findMany();
return response.success({ posts });
},
}),
// A mutation to create a new post
create: igniter.mutation({
path: '/',
method: 'POST',
body: z.object({ title: z.string(), content: z.string() }),
handler: async ({ request, context, response }) => {
const newPost = await context.database.post.create({ data: request.body });
// This is the key part!
// We return a successful response AND tell the client
// to revalidate any queries associated with the 'posts.list' key.
return response.created(newPost).revalidate(['posts.list']);
},
}),
},
});
Step 2: The UI Component (Frontend)
On the frontend, you simply use the useQuery
hook as you normally would. No extra code is needed. The hook automatically listens for revalidation events.
// app/components/PostsList.tsx
'use client';
import { api } from '@/igniter.client';
function PostsList() {
const listPostsQuery = api.posts.list.useQuery();
if (listPostsQuery.isLoading) {
return <div>Loading posts...</div>;
}
return (
<ul>
{listPostsQuery.data?.posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
What Happens?
- A user is viewing the
PostsList
component. - Another user (or the same user in a different tab) creates a new post, triggering the
create
mutation. - The backend responds with
201 Created
and publishes arevalidate
event for theposts.list
key to the central SSE channel. - The
IgniterProvider
on the client receives this event. - It notifies the
api.posts.list.useQuery()
hook that its data is now stale. - The hook automatically refetches the data from the
/posts
endpoint. - The
PostsList
component re-renders with the new post, all in real-time.
Scoped Revalidation: Targeting Specific Clients
Broadcasting a revalidation event to every single client is not always desirable, especially for user-specific data (e.g., updating a user's own profile). Igniter.js allows you to target specific clients by using scopes.
A scope is simply a string identifier that you associate with a client connection. Common scopes include a user's ID (user:123
), their roles (role:admin
), or a tenant ID (tenant:abc-corp
).
Step 1: Define Scopes on the Client
In your IgniterProvider
, you must define the scopes for the current client using the getScopes
prop. This function receives the client context and should return an array of scope strings.
// app/providers.tsx
import { IgniterProvider } from '@igniter-js/core/client';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<IgniterProvider
// ... other props
// Define the scopes for this connection
getScopesIds={() => {
'use server'
// ctx is the client context (e.g., from useSession())
const session = getSession()
return [
`user:${session.user.id}`, // Scope for this specific user
...session.user.roles.map(role => `role:${role}`) // Scopes for each of the user's roles
];
}}
>
{children}
</IgniterProvider>
);
}
Step 2: Publish to Scopes on the Backend
Now, in your mutation, you can pass a function as the second argument to .revalidate()
. This function receives the action's context
and must return an array of scope strings to target. The revalidation event will only be sent to clients whose scopes match.
// In a user profile update mutation
updateProfile: igniter.mutation({
path: '/profile',
method: 'PATCH',
body: z.object({ name: z.string() }),
use: [auth], // Auth procedure adds `user` to context
handler: async ({ request, context, response }) => {
const updatedUser = await context.database.user.update({
where: { id: context.auth.user.id },
data: { name: request.body.name },
});
// This revalidation will only be sent to the user whose ID matches.
return response.success(updatedUser).revalidate(
['users.getProfile'], // The query key to invalidate
(ctx) => [`user:${ctx.auth.user.id}`] // The target scopes
);
}
}),
This ensures that when a user updates their profile, only their own client sessions will refetch the profile data, making your real-time updates efficient and secure.
2. Custom Data Streams
For features like a live notification feed or a chat, you need to push arbitrary data to clients. This is done by creating a dedicated streamable query.
Step 1: Create a Streamable Query (Backend)
Define a query
action and set the stream
property to true
. This tells Igniter.js that this endpoint is not for fetching data via a single request, but for opening a persistent subscription channel.
The channel name will be automatically created from the query key (e.g., api.notifications.stream
-> 'notifications.stream'
).
// src/features/notifications/controllers/notification.controller.ts
import { igniter } from '@/igniter';
export const notificationController = igniter.controller({
path: '/notifications',
actions: {
// This action establishes a subscription channel.
stream: igniter.query({
path: '/stream',
stream: true, // Mark this action as a stream
// The handler can be used for authentication or to send an initial confirmation message.
handler: ({ response }) => {
return response.success({ status: 'Connected to notifications stream' });
},
}),
},
});
Step 2: Subscribe to the Stream (Frontend)
In your React component, use the useStream
hook to subscribe to the channel defined in the backend.
// app/components/NotificationsBell.tsx
'use client';
import { useState } from 'react';
import { api } from '@/igniter.client';
function NotificationsBell() {
const [notifications, setNotifications] = useState<string[]>([]);
const [isConnected, setIsConnected] = useState(false);
// Subscribe to the stream
api.notifications.stream.useStream({
onConnect: () => {
console.log('Successfully connected to notifications stream!');
setIsConnected(true);
},
// This callback runs every time the server sends a message
onMessage: (newMessage) => {
console.log('New notification received:', newMessage);
setNotifications((prev) => [...prev, newMessage.text]);
},
onError: (error) => {
console.error('Stream error:', error);
setIsConnected(false);
}
});
return (
<div>
<span>{isConnected ? '🟢' : '🔴'}</span>
<span>{notifications.length}</span>
{/* ... render notifications */}
</div>
);
}
Step 3: Publish to the Stream (Backend)
From anywhere in your backend application, you can now publish messages to this stream using the igniter.realtime.publish()
method.
// For example, in another action that triggers a notification
someOtherAction: igniter.mutation({
handler: async ({ context, response }) => {
// ... do some work ...
// Now, publish a message to the 'notifications.stream' channel.
// All clients subscribed via `useStream` will receive this data.
igniter.realtime.publish('notifications.stream', {
text: 'Your report is ready for download!',
link: '/reports/123',
});
return response.success({ status: 'ok' });
},
}),
This completes the loop, allowing you to push any data you want to your clients in real-time.
Next Steps
- useRealtime - Learn about the client-side realtime hook
- IgniterProvider - Configure the realtime provider
- Igniter.js Store - Explore caching and messaging