PocketBase includes built-in realtime capabilities using Server-Sent Events (SSE), allowing clients to receive live updates when records are created, updated, or deleted.
How realtime works
The realtime system uses a publish-subscribe pattern:
Clients connect via SSE to /api/realtime
Clients subscribe to specific collections or records
Server broadcasts events when data changes
Clients receive updates in real-time
┌─────────┐ ┌─────────────┐ ┌──────────┐
│ Client │─────────────▶│ PocketBase │◀─────────────│ Client B │
│ A │ Subscribe │ Realtime │ Subscribe │ │
└─────────┘ │ Broker │ └──────────┘
│ └─────────────┘ │
│ │ │
│◀────── CREATE ──────────┤ │
│◀────── UPDATE ──────────┤───────── UPDATE ──────────▶│
│◀────── DELETE ──────────┤───────── DELETE ──────────▶│
Subscription topics
Clients can subscribe to different topics:
Collection subscriptions
// Subscribe to all records in a collection
client . subscribe ( 'posts' )
// Subscribe to multiple collections
client . subscribe ( 'posts' , 'comments' , 'users' )
Record subscriptions
// Subscribe to a specific record
client . subscribe ( 'posts/RECORD_ID' )
// Subscribe to multiple specific records
client . subscribe ( 'posts/RECORD_ID_1' , 'posts/RECORD_ID_2' )
Wildcard subscriptions
// Subscribe to all collections
client . subscribe ( '*' )
Wildcard subscriptions can be resource-intensive. Use them sparingly and consider implementing server-side limits.
Realtime events
Three types of events are broadcast:
CREATE event
Triggered when a new record is created:
{
"action" : "create" ,
"record" : {
"id" : "RECORD_ID" ,
"collectionId" : "COLLECTION_ID" ,
"collectionName" : "posts" ,
"title" : "New Post" ,
"created" : "2024-01-01 12:00:00.000Z" ,
"updated" : "2024-01-01 12:00:00.000Z"
}
}
UPDATE event
Triggered when a record is updated:
{
"action" : "update" ,
"record" : {
"id" : "RECORD_ID" ,
"collectionId" : "COLLECTION_ID" ,
"collectionName" : "posts" ,
"title" : "Updated Post" ,
"created" : "2024-01-01 12:00:00.000Z" ,
"updated" : "2024-01-01 12:30:00.000Z"
}
}
DELETE event
Triggered when a record is deleted:
{
"action" : "delete" ,
"record" : {
"id" : "RECORD_ID" ,
"collectionId" : "COLLECTION_ID" ,
"collectionName" : "posts"
}
}
Server-Side Events (SSE) connection
Realtime uses SSE for client-server communication:
Connection lifecycle
app . OnRealtimeConnectRequest (). Bind ( & hook . Handler [ * core . RealtimeConnectRequestEvent ]{
Func : func ( e * core . RealtimeConnectRequestEvent ) error {
// Called when client connects
app . Logger (). Info ( "Client connected" , "id" , e . Client . Id ())
return e . Next ()
},
})
Connection properties
type RealtimeConnectRequestEvent struct {
* RequestEvent
Client subscriptions . Client
IdleTimeout time . Duration // Default: 5 minutes
}
Client identification
Each connection gets a unique client ID:
{
"name" : "PB_CONNECT" ,
"data" : {
"clientId" : "CLIENT_ID_STRING"
}
}
Clients must send this ID when subscribing to topics.
Managing subscriptions
Setting subscriptions
Clients send a POST request to update their subscriptions:
POST /api/realtime
Content-Type : application/json
{
"clientId" : "CLIENT_ID" ,
"subscriptions" : [
"posts" ,
"comments/RECORD_ID" ,
"users"
]
}
Each subscription request replaces all previous subscriptions for that client. To unsubscribe from everything, send an empty array.
Subscription validation
app . OnRealtimeSubscribeRequest (). Bind ( & hook . Handler [ * core . RealtimeSubscribeRequestEvent ]{
Func : func ( e * core . RealtimeSubscribeRequestEvent ) error {
// Validate subscriptions
for _ , sub := range e . Subscriptions {
if strings . HasPrefix ( sub , "admin_" ) {
if e . Auth == nil || ! e . Auth . GetBool ( "isAdmin" ) {
return errors . New ( "unauthorized subscription" )
}
}
}
return e . Next ()
},
})
Subscription limits
Maximum subscriptions per client: 1000
Maximum subscription topic length: 2500 characters
Client ID length: 1-255 characters
Authentication and authorization
Auth state management
Realtime respects authentication state:
// Auth state is stored per client
clientAuth , _ := client . Get ( apis . RealtimeClientAuthKey ).( * core . Record )
if clientAuth != nil {
// Client is authenticated
userId := clientAuth . Id
}
Auth upgrades
Clients can upgrade from guest to authenticated:
// Initial connection (guest)
const client = new PocketBase ( 'http://localhost:8090' )
// Authenticate
await client . collection ( 'users' ). authWithPassword ( 'user@example.com' , 'password' )
// Subscribe with auth (automatically includes auth token)
client . subscribe ( 'private_collection' )
Clients cannot switch between different authenticated users without reconnecting. Auth downgrades (from authenticated to guest) are also forbidden.
API rules apply
Realtime events respect collection API rules:
// Only authenticated users receive updates
collection . ListRule = types . Pointer ( "@request.auth.id != ''" )
// Users only see their own records
collection . ListRule = types . Pointer ( "@request.auth.id = author" )
If a client doesn’t have permission to view a record, they won’t receive realtime events for it.
Server-side implementation
Broadcasting custom events
app . SubscriptionsBroker (). Broadcast ( & subscriptions . Message {
Name : "custom_event" ,
Data : [] byte ( `{"message":"Hello from server"}` ),
})
Sending to specific clients
client , err := app . SubscriptionsBroker (). ClientById ( "CLIENT_ID" )
if err != nil {
return err
}
client . Send ( subscriptions . Message {
Name : "notification" ,
Data : [] byte ( `{"text":"You have a new message"}` ),
})
Managing client state
// Store custom data on client
client . Set ( "userId" , "USER_ID" )
client . Set ( "permissions" , [] string { "read" , "write" })
// Retrieve custom data
userId := client . Get ( "userId" ).( string )
// Remove data
client . Unset ( "userId" )
Iterating over clients
// Get all clients in chunks
chunks := app . SubscriptionsBroker (). ChunkedClients ( 100 )
for _ , chunk := range chunks {
for _ , client := range chunk {
// Process each client
clientId := client . Id ()
subscriptions := client . Subscriptions ()
}
}
Realtime hooks
Message hooks
app . OnRealtimeMessageSend (). Bind ( & hook . Handler [ * core . RealtimeMessageEvent ]{
Func : func ( e * core . RealtimeMessageEvent ) error {
// Intercept or modify messages before sending
app . Logger (). Debug (
"Sending message" ,
"client" , e . Client . Id (),
"message" , e . Message . Name ,
)
return e . Next ()
},
})
Connection hooks
app . OnRealtimeConnectRequest (). Bind ( & hook . Handler [ * core . RealtimeConnectRequestEvent ]{
Func : func ( e * core . RealtimeConnectRequestEvent ) error {
// Custom connection logic
return e . Next ()
},
})
app . OnRealtimeDisconnectRequest (). Bind ( & hook . Handler [ * core . RealtimeDisconnectRequestEvent ]{
Func : func ( e * core . RealtimeDisconnectRequestEvent ) error {
// Cleanup on disconnect
return e . Next ()
},
})
Subscription hooks
app . OnRealtimeSubscribeRequest (). Bind ( & hook . Handler [ * core . RealtimeSubscribeRequestEvent ]{
Func : func ( e * core . RealtimeSubscribeRequestEvent ) error {
// Log subscription changes
app . Logger (). Info (
"Subscriptions updated" ,
"client" , e . Client . Id (),
"subscriptions" , e . Subscriptions ,
)
return e . Next ()
},
})
Connection management
Idle timeout
Connections timeout after inactivity:
app . OnRealtimeConnectRequest (). Bind ( & hook . Handler [ * core . RealtimeConnectRequestEvent ]{
Func : func ( e * core . RealtimeConnectRequestEvent ) error {
// Set custom idle timeout
e . IdleTimeout = 10 * time . Minute
return e . Next ()
},
})
Connection limits
Implement connection limits:
var activeConnections atomic . Int32
app . OnRealtimeConnectRequest (). Bind ( & hook . Handler [ * core . RealtimeConnectRequestEvent ]{
Func : func ( e * core . RealtimeConnectRequestEvent ) error {
count := activeConnections . Add ( 1 )
defer activeConnections . Add ( - 1 )
if count > 1000 {
return errors . New ( "too many connections" )
}
return e . Next ()
},
})
Graceful disconnection
// Server-side disconnect
client , _ := app . SubscriptionsBroker (). ClientById ( "CLIENT_ID" )
app . SubscriptionsBroker (). Unregister ( client . Id ())
Client libraries
PocketBase provides official client SDKs with realtime support:
JavaScript/TypeScript
import PocketBase from 'pocketbase'
const pb = new PocketBase ( 'http://localhost:8090' )
// Subscribe to collection
pb . collection ( 'posts' ). subscribe ( '*' , ( e ) => {
console . log ( e . action ) // create, update, delete
console . log ( e . record )
})
// Subscribe to specific record
pb . collection ( 'posts' ). subscribe ( 'RECORD_ID' , ( e ) => {
console . log ( e . record )
})
// Unsubscribe
pb . collection ( 'posts' ). unsubscribe ()
Dart/Flutter
import 'package:pocketbase/pocketbase.dart' ;
final pb = PocketBase ( 'http://localhost:8090' );
// Subscribe
pb. collection ( 'posts' ). subscribe ( '*' , (e) {
print (e.action);
print (e.record);
});
// Unsubscribe
pb. collection ( 'posts' ). unsubscribe ();
Subscribe only to the collections and records you need. Overly broad subscriptions waste bandwidth and processing power.
Batch updates when possible
If you’re making multiple record changes, consider batching them to reduce the number of realtime events.
Implement client-side throttling
For high-frequency updates, throttle or debounce client-side handlers to avoid UI performance issues.
Track active realtime connections and implement limits to prevent resource exhaustion.
Enable HTTP compression on your reverse proxy to reduce bandwidth usage for realtime connections.
Always unsubscribe when components unmount or views change to prevent memory leaks.
Debugging realtime
Enable debug logging
app . OnRealtimeConnectRequest (). Bind ( & hook . Handler [ * core . RealtimeConnectRequestEvent ]{
Func : func ( e * core . RealtimeConnectRequestEvent ) error {
e . App . Logger (). Debug (
"Realtime connection" ,
"clientId" , e . Client . Id (),
"auth" , e . Auth != nil ,
)
return e . Next ()
},
})
Monitor active connections
// Get total client count
clients := app . SubscriptionsBroker (). Clients ()
total := len ( clients )
// Get client info
for clientId , client := range clients {
subs := client . Subscriptions ()
fmt . Printf ( "Client %s : %d subscriptions \n " , clientId , len ( subs ))
}
Test with curl
# Connect to realtime
curl -N http://localhost:8090/api/realtime
# Subscribe (in another terminal)
curl -X POST http://localhost:8090/api/realtime \
-H "Content-Type: application/json" \
-d '{"clientId":"CLIENT_ID","subscriptions":["posts"]}'