Skip to main content
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

name
string
required
Record API name (must have subscriptions enabled)
record
string
required
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:
  1. Record created/updated/deleted
  2. Access rule evaluated for current user
  3. Event sent only if access granted
  4. 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.

Performance Considerations

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

  1. Check if subscriptions are enabled in API config
  2. Verify authentication token is valid
  3. Confirm user has read access to the record
  4. Check if table (not view) is being used
  5. Verify filters aren’t excluding all events

Connection Drops Frequently

  1. Implement automatic reconnection
  2. Check network stability
  3. Verify server keep-alive settings
  4. Review server-side connection limits

Missing Updates

  1. Events are not guaranteed delivery (use SSE, not WebSocket for reliability)
  2. Check if updates occurred during disconnection
  3. Implement periodic polling as fallback
  4. Verify event filters aren’t excluding updates

Best Practices

  1. Implement reconnection logic with exponential backoff
  2. Clean up subscriptions when components unmount
  3. Use table-level subscriptions sparingly - they can be resource-intensive
  4. Filter events client-side if server-side filtering isn’t sufficient
  5. Combine with polling for critical data that can’t afford missed updates
  6. Monitor connection health with keep-alive messages
  7. Test access control to ensure users only see authorized data
  8. Limit concurrent subscriptions per client to avoid resource exhaustion

Error Responses

403
Forbidden
User doesn’t have read access or subscriptions disabled
404
Not Found
Record or API not found
405
Method Not Allowed
API doesn’t support subscriptions or uses a view instead of table

Build docs developers (and LLMs) love