Skip to content

Add realtime subscriptions

Generate typed realtime subscription helpers for PocketBase SSE.

The @karnak19/pbkit-realtime package provides a plugin that generates typed realtime subscription helpers using PocketBase’s built-in SSE system.

The plugin runs at generation time, so it is a dev dependency. pocketbase is the runtime peer (already required by the generated client).

Terminal window
bun add -d @karnak19/pbkit-realtime

Add the plugin to your pbkit.config.ts:

import { realtimePlugin } from "@karnak19/pbkit-realtime"
export default {
input: "https://my-pb.example.com",
output: "./src/generated",
plugins: [realtimePlugin],
}

After running bunx pbkit generate, a realtime.gen.ts file is created alongside types.gen.ts, client.gen.ts, and sdk.gen.ts.

The plugin generates shared types and one subscribeTo{Collection}() function per non-excluded collection:

export type RealtimeAction = "create" | "update" | "delete"
export interface RealtimeEvent<T> {
action: RealtimeAction
record: T
}
export function subscribeToArticles(
callback: (event: RealtimeEvent<ArticlesRecord>) => void,
options?: { filter?: string; id?: string },
): Promise<() => Promise<void>>

Each function:

  • Accepts a typed callback receiving RealtimeEvent<{Collection}Record>
  • Accepts optional filter (PocketBase filter string) and id (subscribe to a specific record)
  • Returns Promise<() => Promise<void>> — resolves with an async unsubscribe callback (PocketBase’s UnsubscribeFunc); await it to surface unsubscribe errors
import { subscribeToArticles } from "./generated/realtime.gen"
const unsub = await subscribeToArticles((event) => {
if (event.action === "create") {
console.log("New article:", event.record.title)
}
if (event.action === "update") {
console.log("Updated article:", event.record.id)
}
if (event.action === "delete") {
console.log("Deleted article:", event.record.id)
}
})
// Later, when you no longer need the subscription
await unsub()
const unsub = await subscribeToArticles(
(event) => {
console.log("Published article changed:", event.record.title)
},
{ filter: 'status = "published"' },
)
const unsub = await subscribeToArticles(
(event) => {
console.log("Article updated:", event.record.title)
},
{ id: "RECORD_ID" },
)
import { subscribeToComments } from "./generated/realtime.gen"
import { useEffect, useState } from "react"
function LiveComments({ articleId }: { articleId: string }) {
const [comments, setComments] = useState([])
useEffect(() => {
let unsub: (() => Promise<void>) | undefined
subscribeToComments(
(event) => {
if (event.action === "create") {
setComments((prev) => [...prev, event.record])
}
},
{ filter: `article = "${articleId}"` },
).then((fn) => { unsub = fn })
return () => { unsub?.() }
}, [articleId])
return (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.content}</li>
))}
</ul>
)
}

Avoid interpolating untrusted input directly into filter strings. Prefer constructing filters server-side or validating IDs before inserting them.

The plugin respects the collections config — excluded collections won’t generate subscription functions:

export default {
collections: {
logs: { exclude: true },
},
plugins: [realtimePlugin],
}

The generated helpers use the PocketBase client directly via client.collection(name).subscribe(). No framework-specific imports — works with React, Vue, Svelte, Solid, or any JavaScript environment.

All plugins can be used together:

import { tanstack } from "@karnak19/pbkit-tanstack"
import { zodPlugin } from "@karnak19/pbkit-zod"
import { realtimePlugin } from "@karnak19/pbkit-realtime"
export default {
input: "https://my-pb.example.com",
output: "./src/generated",
plugins: [tanstack({ framework: "react" }), zodPlugin, realtimePlugin],
}