> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/pocketbase/pocketbase/llms.txt
> Use this file to discover all available pages before exploring further.

# Realtime

> Real-time data synchronization with Server-Sent Events (SSE)

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

```javascript theme={null}
// Subscribe to all records in a collection
client.subscribe('posts')

// Subscribe to multiple collections
client.subscribe('posts', 'comments', 'users')
```

### Record subscriptions

```javascript theme={null}
// 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

```javascript theme={null}
// Subscribe to all collections
client.subscribe('*')
```

<Warning>
  Wildcard subscriptions can be resource-intensive. Use them sparingly and consider implementing server-side limits.
</Warning>

## Realtime events

Three types of events are broadcast:

### CREATE event

Triggered when a new record is created:

```json theme={null}
{
  "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:

```json theme={null}
{
  "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:

```json theme={null}
{
  "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

```go theme={null}
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

```go theme={null}
type RealtimeConnectRequestEvent struct {
    *RequestEvent
    Client      subscriptions.Client
    IdleTimeout time.Duration  // Default: 5 minutes
}
```

### Client identification

Each connection gets a unique client ID:

```json theme={null}
{
  "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:

```http theme={null}
POST /api/realtime
Content-Type: application/json

{
  "clientId": "CLIENT_ID",
  "subscriptions": [
    "posts",
    "comments/RECORD_ID",
    "users"
  ]
}
```

<Note>
  Each subscription request **replaces** all previous subscriptions for that client. To unsubscribe from everything, send an empty array.
</Note>

### Subscription validation

```go theme={null}
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:

```go theme={null}
// 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:

```javascript theme={null}
// 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')
```

<Warning>
  Clients **cannot** switch between different authenticated users without reconnecting. Auth downgrades (from authenticated to guest) are also forbidden.
</Warning>

### API rules apply

Realtime events respect collection API rules:

```go theme={null}
// 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

```go theme={null}
app.SubscriptionsBroker().Broadcast(&subscriptions.Message{
    Name: "custom_event",
    Data: []byte(`{"message":"Hello from server"}`),
})
```

### Sending to specific clients

```go theme={null}
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

```go theme={null}
// 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

```go theme={null}
// 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

```go theme={null}
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

```go theme={null}
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

```go theme={null}
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:

```go theme={null}
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:

```go theme={null}
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

```go theme={null}
// 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

```javascript theme={null}
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

```dart theme={null}
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

<AccordionGroup>
  <Accordion title="Limit subscription scope">
    Subscribe only to the collections and records you need. Overly broad subscriptions waste bandwidth and processing power.
  </Accordion>

  <Accordion title="Batch updates when possible">
    If you're making multiple record changes, consider batching them to reduce the number of realtime events.
  </Accordion>

  <Accordion title="Implement client-side throttling">
    For high-frequency updates, throttle or debounce client-side handlers to avoid UI performance issues.
  </Accordion>

  <Accordion title="Monitor connection count">
    Track active realtime connections and implement limits to prevent resource exhaustion.
  </Accordion>

  <Accordion title="Use SSE compression">
    Enable HTTP compression on your reverse proxy to reduce bandwidth usage for realtime connections.
  </Accordion>

  <Accordion title="Clean up subscriptions">
    Always unsubscribe when components unmount or views change to prevent memory leaks.
  </Accordion>
</AccordionGroup>

## Debugging realtime

### Enable debug logging

```go theme={null}
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

```go theme={null}
// 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

```bash theme={null}
# 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"]}'
```
