Skip to main content
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:
  1. Clients connect via SSE to /api/realtime
  2. Clients subscribe to specific collections or records
  3. Server broadcasts events when data changes
  4. 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();

Performance considerations

Subscribe only to the collections and records you need. Overly broad subscriptions waste bandwidth and processing power.
If you’re making multiple record changes, consider batching them to reduce the number of realtime events.
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"]}'