Realtime API - JavaScript SDK Documentation
Overview
The Realtime API enables real-time updates for collection records using Server-Sent Events (SSE). It allows you to subscribe to changes in collections or specific records and receive instant notifications when records are created, updated, or deleted.
Key Features:
- Real-time notifications for record changes
- Collection-level and record-level subscriptions
- Automatic connection management and reconnection
- Authorization support
- Subscription options (expand, custom headers, query params)
- Event-driven architecture
Backend Endpoints:
GET /api/realtime- Establish SSE connectionPOST /api/realtime- Set subscriptions
How It Works
- Connection: The SDK establishes an SSE connection to
/api/realtime - Client ID: Server sends
PB_CONNECTevent with a uniqueclientId - Subscriptions: Client submits subscription topics via POST request
- Events: Server sends events when matching records change
- Reconnection: SDK automatically reconnects on connection loss
Basic Usage
Subscribe to Collection Changes
Subscribe to all changes in a collection:
import BosBase from 'bosbase';
const pb = new BosBase('http://127.0.0.1:8090');
// Subscribe to all changes in the 'posts' collection
const unsubscribe = await pb.collection('posts').subscribe('*', function (e) {
console.log('Action:', e.action); // 'create', 'update', or 'delete'
console.log('Record:', e.record); // The record data
});
// Later, unsubscribe
await unsubscribe();
Subscribe to Specific Record
Subscribe to changes for a single record:
// Subscribe to changes for a specific post
await pb.collection('posts').subscribe('RECORD_ID', function (e) {
console.log('Record changed:', e.record);
console.log('Action:', e.action);
});
Multiple Subscriptions
You can subscribe multiple times to the same or different topics:
// Subscribe to multiple records
const unsubscribe1 = await pb.collection('posts').subscribe('RECORD_ID_1', handleChange);
const unsubscribe2 = await pb.collection('posts').subscribe('RECORD_ID_2', handleChange);
const unsubscribe3 = await pb.collection('posts').subscribe('*', handleAllChanges);
function handleChange(e) {
console.log('Change event:', e);
}
function handleAllChanges(e) {
console.log('Collection-wide change:', e);
}
// Unsubscribe individually
await unsubscribe1();
await unsubscribe2();
await unsubscribe3();
Event Structure
Each event received contains:
{
action: 'create' | 'update' | 'delete', // Action type
record: { // Record data
id: 'RECORD_ID',
collectionId: 'COLLECTION_ID',
collectionName: 'collection_name',
created: '2023-01-01 00:00:00.000Z',
updated: '2023-01-01 00:00:00.000Z',
// ... other fields
}
}
PB_CONNECT Event
When the connection is established, you receive a PB_CONNECT event:
await pb.realtime.subscribe('PB_CONNECT', function (e) {
console.log('Connected! Client ID:', e.clientId);
// e.clientId - unique client identifier
});
Subscription Topics
Collection-Level Subscription
Subscribe to all changes in a collection:
// Wildcard subscription - all records in collection
await pb.collection('posts').subscribe('*', handler);
Access Control: Uses the collection’s ListRule to determine if the subscriber has access to receive events.
Record-Level Subscription
Subscribe to changes for a specific record:
// Specific record subscription
await pb.collection('posts').subscribe('RECORD_ID', handler);
Access Control: Uses the collection’s ViewRule to determine if the subscriber has access to receive events.
Subscription Options
You can pass additional options when subscribing:
await pb.collection('posts').subscribe('*', handler, {
// Query parameters (for API rule filtering)
query: {
'filter': 'status = "published"',
'expand': 'author',
},
// Custom headers
headers: {
'X-Custom-Header': 'value',
},
});
Expand Relations
Expand relations in the event data:
await pb.collection('posts').subscribe('RECORD_ID', function (e) {
console.log(e.record.expand.author); // Author relation expanded
}, {
query: {
expand: 'author,categories',
},
});
Filter with Query Parameters
Use query parameters for API rule filtering:
await pb.collection('posts').subscribe('*', handler, {
query: {
filter: 'status = "published"',
},
});
Unsubscribing
Unsubscribe from Specific Topic
// Remove all subscriptions for a specific record
await pb.collection('posts').unsubscribe('RECORD_ID');
// Remove all wildcard subscriptions for the collection
await pb.collection('posts').unsubscribe('*');
Unsubscribe from All
// Unsubscribe from all subscriptions in the collection
await pb.collection('posts').unsubscribe();
// Or unsubscribe from everything
await pb.realtime.unsubscribe();
Unsubscribe Using Returned Function
const unsubscribe = await pb.collection('posts').subscribe('*', handler);
// Later...
await unsubscribe(); // Removes this specific subscription
Connection Management
Connection Status
Check if the realtime connection is established:
if (pb.realtime.isConnected) {
console.log('Realtime connected');
} else {
console.log('Realtime disconnected');
}
Disconnect Handler
Handle disconnection events:
pb.realtime.onDisconnect = function (activeSubscriptions) {
if (activeSubscriptions.length > 0) {
console.log('Connection lost, but subscriptions remain:', activeSubscriptions);
// Connection will automatically reconnect
} else {
console.log('Intentionally disconnected (no active subscriptions)');
}
};
Automatic Reconnection
The SDK automatically:
- Reconnects when the connection is lost
- Resubmits all active subscriptions
- Handles network interruptions gracefully
- Closes connection after 5 minutes of inactivity (server-side timeout)
Authorization
Authenticated Subscriptions
Subscriptions respect authentication. If you’re authenticated, events are filtered based on your permissions:
// Authenticate first
await pb.collection('users').authWithPassword('user@example.com', 'password');
// Now subscribe - events will respect your permissions
await pb.collection('posts').subscribe('*', handler);
Authorization Rules
- Collection-level (
*): UsesListRuleto determine access - Record-level: Uses
ViewRuleto determine access - Superusers: Can receive all events (if rules allow)
- Guests: Only receive events they have permission to see
Auth State Changes
When authentication state changes, you may need to resubscribe:
// After login/logout, resubscribe to update permissions
await pb.collection('users').authWithPassword('user@example.com', 'password');
// Re-subscribe to update auth state in realtime connection
await pb.collection('posts').subscribe('*', handler);
Advanced Examples
Example 1: Real-time Chat
// Subscribe to messages in a chat room
async function setupChatRoom(roomId) {
const unsubscribe = await pb.collection('messages').subscribe('*', function (e) {
// Filter for this room only
if (e.record.roomId === roomId) {
if (e.action === 'create') {
displayMessage(e.record);
} else if (e.action === 'delete') {
removeMessage(e.record.id);
}
}
}, {
query: {
filter: `roomId = "${roomId}"`,
},
});
return unsubscribe;
}
// Usage
const unsubscribeChat = await setupChatRoom('ROOM_ID');
// Cleanup
await unsubscribeChat();
Example 2: Real-time Dashboard
// Subscribe to multiple collections
async function setupDashboard() {
// Posts updates
await pb.collection('posts').subscribe('*', function (e) {
if (e.action === 'create') {
addPostToFeed(e.record);
} else if (e.action === 'update') {
updatePostInFeed(e.record);
}
}, {
query: {
filter: 'status = "published"',
expand: 'author',
},
});
// Comments updates
await pb.collection('comments').subscribe('*', function (e) {
updateCommentsCount(e.record.postId);
}, {
query: {
expand: 'user',
},
});
}
setupDashboard();
Example 3: User Activity Tracking
// Track changes to a user's own records
async function trackUserActivity(userId) {
await pb.collection('posts').subscribe('*', function (e) {
// Only track changes to user's own posts
if (e.record.author === userId) {
console.log(`Your post ${e.action}:`, e.record.title);
if (e.action === 'update') {
showNotification('Post updated');
}
}
}, {
query: {
filter: `author = "${userId}"`,
},
});
}
await trackUserActivity(pb.authStore.record.id);
Example 4: Real-time Collaboration
// Track when a document is being edited
async function trackDocumentEdits(documentId) {
await pb.collection('documents').subscribe(documentId, function (e) {
if (e.action === 'update') {
const lastEditor = e.record.lastEditor;
const updatedAt = e.record.updated;
// Show who last edited the document
showEditorIndicator(lastEditor, updatedAt);
}
}, {
query: {
expand: 'lastEditor',
},
});
}
Example 5: Connection Monitoring
// Monitor connection state
pb.realtime.onDisconnect = function (activeSubscriptions) {
if (activeSubscriptions.length > 0) {
console.warn('Connection lost, attempting to reconnect...');
showConnectionStatus('Reconnecting...');
}
};
// Monitor connection establishment
await pb.realtime.subscribe('PB_CONNECT', function (e) {
console.log('Connected to realtime:', e.clientId);
showConnectionStatus('Connected');
});
Example 6: Conditional Subscriptions
// Subscribe conditionally based on user state
async function setupConditionalSubscriptions() {
if (pb.authStore.isValid) {
// Authenticated user - subscribe to private posts
await pb.collection('posts').subscribe('*', handler, {
query: {
filter: '@request.auth.id != ""',
},
});
} else {
// Guest user - subscribe only to public posts
await pb.collection('posts').subscribe('*', handler, {
query: {
filter: 'public = true',
},
});
}
}
Example 7: Cleanup on Component Unmount (React/Vue)
// React example
import { useEffect, useRef } from 'react';
function useRealtimeSubscription(collectionName, topic, handler) {
const unsubscribeRef = useRef(null);
useEffect(() => {
let mounted = true;
pb.collection(collectionName).subscribe(topic, async (e) => {
if (mounted) {
handler(e);
}
}).then(unsubscribe => {
unsubscribeRef.current = unsubscribe;
});
return () => {
mounted = false;
if (unsubscribeRef.current) {
unsubscribeRef.current();
}
};
}, [collectionName, topic]);
}
// Usage
function PostsList() {
useRealtimeSubscription('posts', '*', (e) => {
console.log('Post changed:', e);
});
return <div>Posts...</div>;
}
Error Handling
try {
await pb.collection('posts').subscribe('*', handler);
} catch (error) {
if (error.status === 403) {
console.error('Permission denied');
} else if (error.status === 404) {
console.error('Collection not found');
} else {
console.error('Subscription error:', error);
}
}
Best Practices
- Unsubscribe When Done: Always unsubscribe when components unmount or subscriptions are no longer needed
- Handle Disconnections: Implement
onDisconnecthandler for better UX - Filter Server-Side: Use query parameters to filter events server-side when possible
- Limit Subscriptions: Don’t subscribe to more collections than necessary
- Use Record-Level When Possible: Prefer record-level subscriptions over collection-level when you only need specific records
- Monitor Connection: Track connection state for debugging and user feedback
- Handle Errors: Wrap subscriptions in try-catch blocks
- Respect Permissions: Understand that events respect API rules and permissions
Limitations
- Maximum Subscriptions: Up to 1000 subscriptions per client
- Topic Length: Maximum 2500 characters per topic
- Idle Timeout: Connection closes after 5 minutes of inactivity
- Network Dependency: Requires stable network connection
- Browser Support: SSE requires modern browsers (not available in IE)
Troubleshooting
Connection Not Establishing
// Check connection status
console.log('Connected:', pb.realtime.isConnected);
// Manually trigger connection
await pb.collection('posts').subscribe('*', handler);
Events Not Received
- Check API rules - you may not have permission
- Verify subscription is active
- Check network connectivity
- Review server logs for errors
Memory Leaks
Always unsubscribe:
// Good
const unsubscribe = await pb.collection('posts').subscribe('*', handler);
// ... later
await unsubscribe();
// Bad - no cleanup
await pb.collection('posts').subscribe('*', handler);
// Never unsubscribed - memory leak!
Related Documentation
- API Records - CRUD operations
- Collections - Collection configuration
- API Rules and Filters - Understanding API rules