realtime
Define a live channel once and push updates to every open screen, over WebSockets, Socket.IO or server events.
What it is
@kitr/realtime pushes events from the server to the browser over channels. defineChannel declares a channel's params and events with schemas. The server emits; useChannel subscribes; each event's effects update the query cache so components re-render.
Runs on both sides. For one-off calls that return a value, use actions or API routes.
Install
kitr add realtimeFirst time? Run npm i -g @kitr/kitr once. Inside a project, the global kitr runs your project's own version.
Adds @kitr/realtime with crossws and partysocket, config/realtime.ts, an example channel, the client in app/realtime.ts and a .ws() route. Adds server first if missing. Every change.
Quick start
A channel both ends import, an emit on the server, a subscription in a component. First the channel:
// app/channels/projects.ts
import { defineChannel } from '@kitr/realtime'
import { z } from 'zod'
export const projectSchema = z.object({ id: z.string(), name: z.string(), teamId: z.string() })
export type Project = z.infer<typeof projectSchema>
export const projectChannel = defineChannel({
name: 'projects',
params: z.object({ id: z.string() }),
events: {
updated: {
schema: projectSchema,
effects: (project, { id }) => ({ set: [{ key: ['projects', 'detail', id], data: project }] }),
},
},
})
Add projectChannel to the .ws('/ws', { channels: [...] }) route in server/routes.ts, then emit from the controller that renames a project:
// server/controllers/projects_controller.ts
app.container.use('realtime').use(projectChannel, { id: params.id }).emit('updated', project)
Subscribe in the component that shows the project:
// app/features/projects/project-title.tsx
import { useQuery } from '@kitr/query'
import { useChannel } from '@kitr/realtime/react'
import { type Project, projectChannel } from '#app/channels/projects'
import { realtime } from '#app/realtime'
export function ProjectTitle({ id }: { id: string }) {
useChannel(realtime(), projectChannel, { id })
const { data } = useQuery<Project>({ queryKey: ['projects', 'detail', id], enabled: false })
return <h1>{data?.name ?? 'Loading'}</h1>
}
When Grace saves a new name, every subscribed client validates the event and writes it into the cache. Ada's heading re-renders. No refetch. Next in the guide: Push your first update.
Built on
| Transport | Built on | Use it for |
|---|---|---|
| WebSockets (default) | crossws on the h3 server | Most apps, on Node, Bun, Deno and Workers. |
| Socket.IO | Socket.IO | Existing Socket.IO clients, rooms and its reconnection behavior. |
| Server-Sent Events | The browser's EventSource |
Hosts that block WebSockets, or one-way updates. |
kitr adds defineChannel, schema validation, authentication and authorization, cache effects, the hooks, reconnection and the route extension.
Problems it solves
- Untyped messages. Define a channel.
- Screens that do not update. Update the cache from events.
- Anyone subscribing to anything. Authorize a subscription.
- Connection state nobody shows. Show the connection status.
- Changing transport later. Transports.
- Several server instances. Run more than one server instance.
Further reading
- Guide.
- API reference.
- crossws and Socket.IO.
- Query and Server.
- Getting started.