The Subscriptions API enables real-time updates for records using Server-Sent Events (SSE) or WebSockets, allowing clients to receive instant notifications when records are created, updated, or deleted.
Base Path: /api/records/v1
Subscribe to Record Changes
Subscribe to real-time updates for a specific record.
GET /api/records/v1/{name}/subscribe/{record}
curl -X GET "https://your-instance.com/api/records/v1/posts/subscribe/abc123" \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-H "Accept: text/event-stream"
Path Parameters
Record API name (must have subscriptions enabled)
Record ID to subscribe to
Query Parameters
Filters can be applied to subscription queries using the same syntax as list queries:
# Subscribe only to specific field changes
curl "https://your-instance.com/api/records/v1/posts/subscribe/abc123?filter[status]=published"
Response (SSE Stream)
The endpoint returns an event stream with Server-Sent Events:
event: message
data: {"Insert":{"id":"abc123","title":"New Post","status":"draft"}}
event: message
data: {"Update":{"id":"abc123","title":"Updated Post","status":"published"}}
event: message
data: {"Delete":{"id":"abc123"}}
Event Types
Insert Event
Fired when a new record is created:
{
"Insert": {
"id": "abc123",
"title": "My New Post",
"content": "Hello, world!",
"created_at": "2026-03-07T12:00:00Z"
}
}
Update Event
Fired when a record is modified:
{
"Update": {
"id": "abc123",
"title": "Updated Title",
"status": "published",
"updated_at": "2026-03-07T13:00:00Z"
}
}
Delete Event
Fired when a record is deleted:
{
"Delete": {
"id": "abc123",
"deleted_at": "2026-03-07T14:00:00Z"
}
}
Error Event
Fired when an error occurs:
{
"Error": "Subscription terminated: access denied"
}
Connection Management
Keep-Alive
The server sends periodic keep-alive messages to maintain the connection:
event: keep-alive
data: ping
Automatic Reconnection
Clients should implement automatic reconnection with exponential backoff:
function subscribeWithRetry(url, retries = 0) {
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
handleRecordUpdate(data);
};
eventSource.onerror = (error) => {
eventSource.close();
const backoff = Math.min(1000 * Math.pow(2, retries), 30000);
setTimeout(() => subscribeWithRetry(url, retries + 1), backoff);
};
return eventSource;
}
Cleanup
Subscriptions are automatically cleaned up when:
- Client disconnects
- Connection times out
- Table or record is deleted
- Access permissions change
Access Control
Subscriptions respect the same access control rules as read operations.
Table-Level Access
Users must have Read permission to subscribe:
-- ACL configuration
acl_authenticated: [Read]
Row-Level Access
Subscription events are filtered by row-level access rules:
-- Only receive updates for records the user can access
read_access_rule: "_ROW_.owner = _USER_.id"
Real-Time Access Checks
Access is re-evaluated for each event:
- Record created/updated/deleted
- Access rule evaluated for current user
- Event sent only if access granted
- Subscription terminated if access permanently lost
Enabling Subscriptions
Subscriptions must be explicitly enabled in the Record API configuration:
{
"name": "posts",
"table_name": "posts",
"enable_subscriptions": true,
"acl_authenticated": ["Read"],
"read_access_rule": "_ROW_.owner = _USER_.id"
}
Subscriptions only work on tables, not views. The underlying table must support SQLite’s update hooks.
JavaScript/TypeScript Client
Using EventSource (SSE)
const token = 'YOUR_AUTH_TOKEN';
const recordId = 'abc123';
const eventSource = new EventSource(
`https://your-instance.com/api/records/v1/posts/subscribe/${recordId}`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if ('Insert' in data) {
console.log('Record created:', data.Insert);
} else if ('Update' in data) {
console.log('Record updated:', data.Update);
} else if ('Delete' in data) {
console.log('Record deleted:', data.Delete);
} else if ('Error' in data) {
console.error('Subscription error:', data.Error);
}
};
eventSource.onerror = (error) => {
console.error('Connection error:', error);
eventSource.close();
};
// Cleanup
window.addEventListener('beforeunload', () => {
eventSource.close();
});
Using WebSocket
WebSocket support requires the ws feature to be enabled in your TrailBase build.
const token = 'YOUR_AUTH_TOKEN';
const ws = new WebSocket(
`wss://your-instance.com/api/records/v1/posts/subscribe/abc123?token=${token}`
);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Record update:', data);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('WebSocket closed');
};
// Send ping to keep connection alive
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
React Hook Example
import { useEffect, useState } from 'react';
interface Record {
id: string;
title: string;
status: string;
}
function useRecordSubscription(apiName: string, recordId: string, token: string) {
const [record, setRecord] = useState<Record | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const eventSource = new EventSource(
`https://your-instance.com/api/records/v1/${apiName}/subscribe/${recordId}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if ('Insert' in data || 'Update' in data) {
setRecord(data.Insert || data.Update);
} else if ('Delete' in data) {
setRecord(null);
} else if ('Error' in data) {
setError(data.Error);
}
} catch (err) {
setError('Failed to parse event data');
}
};
eventSource.onerror = () => {
setError('Connection error');
eventSource.close();
};
return () => {
eventSource.close();
};
}, [apiName, recordId, token]);
return { record, error };
}
// Usage
function PostViewer({ postId, token }) {
const { record, error } = useRecordSubscription('posts', postId, token);
if (error) return <div>Error: {error}</div>;
if (!record) return <div>Loading...</div>;
return (
<div>
<h1>{record.title}</h1>
<p>Status: {record.status}</p>
</div>
);
}
Filtering Subscription Events
Apply filters to receive only relevant events:
# Only receive events when status is 'published'
curl "https://your-instance.com/api/records/v1/posts/subscribe/abc123?filter[status]=published"
# Combine multiple filters
curl "https://your-instance.com/api/records/v1/posts/subscribe/abc123?filter[status]=published&filter[author]=john"
Filters use the same syntax as the Records list filtering.
Connection Limits
Each subscription maintains an open connection. Consider:
- Server connection limits (configure via TrailBase settings)
- Client browser limits (typically 6 connections per domain)
- Database load from active subscriptions
Batching Updates
For high-frequency updates, consider implementing client-side debouncing:
let updateQueue = [];
let debounceTimer;
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
updateQueue.push(data);
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
processBatch(updateQueue);
updateQueue = [];
}, 100);
};
Subscription Scope
Subscribe to the most specific resource:
- ✅ Subscribe to individual records when possible
- ⚠️ Table-level subscriptions may generate many events
- ❌ Avoid subscribing to rapidly-changing records
Troubleshooting
Subscription Not Receiving Events
- Check if subscriptions are enabled in API config
- Verify authentication token is valid
- Confirm user has read access to the record
- Check if table (not view) is being used
- Verify filters aren’t excluding all events
Connection Drops Frequently
- Implement automatic reconnection
- Check network stability
- Verify server keep-alive settings
- Review server-side connection limits
Missing Updates
- Events are not guaranteed delivery (use SSE, not WebSocket for reliability)
- Check if updates occurred during disconnection
- Implement periodic polling as fallback
- Verify event filters aren’t excluding updates
Best Practices
- Implement reconnection logic with exponential backoff
- Clean up subscriptions when components unmount
- Use table-level subscriptions sparingly - they can be resource-intensive
- Filter events client-side if server-side filtering isn’t sufficient
- Combine with polling for critical data that can’t afford missed updates
- Monitor connection health with keep-alive messages
- Test access control to ensure users only see authorized data
- Limit concurrent subscriptions per client to avoid resource exhaustion
Error Responses
User doesn’t have read access or subscriptions disabled
API doesn’t support subscriptions or uses a view instead of table