Realtime Plugin
The Realtime plugin (pocketpages-plugin-sse
) adds Server-Sent Events (SSE) support to PocketPages, enabling real-time updates and notifications in your application.
Installation
npm install pocketpages-plugin-sse
Configuration
Add the plugin to your +config.js
file:
module.exports = {
plugins: [
'pocketpages-plugin-sse',
// ... other plugins
],
}
Usage
The plugin adds an sseSend()
function to the global context that can be used in two ways:
1. Direct Message Sending
Send a message immediately to all subscribed clients:
<%
// Send to all subscribers of 'chat'
sseSend('chat', 'New message arrived!')
// Send only to specific clients using a filter
sseSend('chat', 'Private message', (clientId, client) => {
return client.get('roomId') === '123'
})
%>
2. Render Capture Mode
When sseSend()
is called with only a channel name (or with a filter function as the second parameter), it captures and broadcasts the rendered output of the template:
<%
// Capture and send everything rendered after this point
sseSend('chat')
// Or with a filter to control who receives the update
sseSend('chat', (clientId, client) => {
return client.get('roomId') === currentRoom.id
})
%>
<div class="message">
<strong><%= user.name %>:</strong> <%= message %>
</div>
This is particularly useful with HTMX integration:
<!-- Client-side template -->
<div hx-ext="pocketbase-sse">
<div sse-swap="chat" hx-swap="beforeend">
<!-- SSE content will be appended here -->
</div>
</div>
<!-- Server-side API endpoint -->
<%
const { message } = body()
sseSend('chat') // Capture mode
%>
<div class="message">
<%= message %>
</div>
The rendered HTML content is automatically JSON-stringified to safely handle newlines and special characters.
Client-Side Integration
1. Connect to the SSE Stream
// Initialize the PocketBase client
const pb = new PocketBase('http://127.0.0.1:8090')
// Subscribe to realtime updates
const unsubscribe = pb.realtime.subscribe('chat', (data) => {
console.log('New chat message:', data)
})
// Later, unsubscribe if needed
unsubscribe()
// Check connection status
if (pb.realtime.isConnected) {
console.log('Connected to realtime API')
}
// Handle disconnects
pb.realtime.onDisconnect = function (subscriptions) {
console.log('Disconnected, active subscriptions:', subscriptions)
}
2. Example: Real-time Chat
<!-- chat.ejs -->
<%
if (request.method === 'POST') {
const message = formData.get('message')
// Store the message
pb().collection('messages').create({
text: message,
user: auth.id,
})
// Broadcast to all chat subscribers
sseSend('chat', {
text: message,
user: auth.name,
})
return { status: 'sent' }
}
%>
<div id="chat-messages"></div>
<form hx-post="/chat" hx-swap="none">
<input type="text" name="message">
<button type="submit">Send</button>
</form>
<script>
const pb = new PocketBase('http://127.0.0.1:8090')
const messages = document.getElementById('chat-messages')
pb.realtime.subscribe('chat', (data) => {
const msg = data
messages.innerHTML += `
<div class="message">
<strong>${msg.user}:</strong> ${msg.text}
</div>
`
})
</script>
Authentication and Filtering
The plugin automatically handles authentication state:
- By default, authenticated users only receive messages intended for them
- Unauthenticated users receive public messages
- Custom filters can override this behavior
Client Object
The client
parameter in filter functions is a PocketBase Subscription Client instance with these key methods:
interface Client {
// Get a stored client value by key
get(key: string): any
// Store a value for the client
set(key: string, value: any): void
// Check if client is subscribed to a topic
hasSubscription(topic: string): boolean
// Send a message to the client
send(message: Message): void
// Get all client subscriptions
subscriptions(): Array<string>
}
A common use case is accessing the client's auth record via client.get('auth')
, which returns a PocketBase Record instance with methods like:
interface Record {
// Get any field value
get(key: string): any
// Common auth record fields
id: string
email(): string
verified(): boolean
username?: string
// Check if user is admin
isSuperuser(): boolean
}
Custom Filter Examples
<%
// Filter by stored room ID
sseSend('room-update', 'User joined', (clientId, client) => {
const roomId = client.get('roomId')
return roomId === currentRoom.id
})
// Filter by auth status
sseSend('private', 'Personal notification', (clientId, client) => {
const auth = client.get('auth') // PocketBase Record
if (!auth) return false
// Check various auth conditions
return (
auth.id === targetUser.id || // Specific user
auth.isSuperuser() || // Admin user
auth.get('role') === 'moderator' // Custom field
)
})
// Filter by subscription
sseSend('broadcast', 'News update', (clientId, client) => {
return client.hasSubscription('news')
})
%>
Best Practices
- Message Format: Always send structured data that can be parsed as JSON
- Error Handling: Add error listeners on the client side
- Reconnection: The EventSource will automatically attempt to reconnect
- Authentication: Use the built-in auth filtering when possible
- Performance: Use specific event types rather than a single generic one
// Good - specific events
events.addEventListener('chat', handleChat)
events.addEventListener('notifications', handleNotifications)
// Less Good - generic event handling
events.onmessage = (e) => {
const data = JSON.parse(e.data)
switch (data.type) {
case 'chat': // ...
case 'notification': // ...
}
}
Default Filtering
The default filter implementation:
// This is the built-in default filter
;(clientId, client) =>
// If user is authenticated, only send to their sessions
api.auth?.id
? client.get('auth')?.id === api.auth?.id
: // If no user is authenticated, send to all clients
true
Common Use Cases
Chat Application
<%
if (request.method === 'POST' && formData.message) {
// Get user info
const userId = request.auth?.id
const username = request.auth?.get('username')
// Create message payload
const payload = stringify({
userId,
username,
message: formData.message,
timestamp: new Date().toISOString()
})
// Broadcast to all chat participants by overriding default filter
sseSend('chat', payload, () => true)
// Optionally store in database
pb().collection('messages').create({
userId,
message: formData.message
})
}
%>
HTMX Integration
As an alternative to using the PocketBase JS SDK directly, you can use the PocketBase HTMX SSE extension for a more declarative approach:
<!-- Include the extension -->
<script src="https://unpkg.com/pocketbase-htmx-ext-sse"></script>
<!-- Chat interface -->
<div hx-ext="pocketbase-sse">
<!-- Messages will be appended here -->
<div sse-swap="chat" hx-swap="beforeend"></div>
<!-- Form automatically triggers SSE update -->
<form
hx-post="/api/chat"
hx-swap="none"
hx-on::after-request="if(event.detail.successful) { this.reset(); }"
>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
And the corresponding server-side endpoint:
<!-- /api/chat.ejs -->
<%
const { message } = body()
sseSend('chat') // Enable render capture mode
%>
<div class="message">
<%= message %>
</div>
This approach:
- Requires less JavaScript code
- Uses a more declarative syntax
- Handles reconnection automatically
- Integrates well with other HTMX features
See the HTMX starter kit for a complete example.