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

# Subscriptions API

> Real-time record updates via Server-Sent Events (SSE) and WebSocket subscriptions

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.

```bash GET /api/records/v1/{name}/subscribe/{record} theme={null}
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

<ParamField path="name" type="string" required>
  Record API name (must have subscriptions enabled)
</ParamField>

<ParamField path="record" type="string" required>
  Record ID to subscribe to
</ParamField>

### Query Parameters

Filters can be applied to subscription queries using the same syntax as list queries:

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

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

```json theme={null}
{
  "Update": {
    "id": "abc123",
    "title": "Updated Title",
    "status": "published",
    "updated_at": "2026-03-07T13:00:00Z"
  }
}
```

### Delete Event

Fired when a record is deleted:

```json theme={null}
{
  "Delete": {
    "id": "abc123",
    "deleted_at": "2026-03-07T14:00:00Z"
  }
}
```

### Error Event

Fired when an error occurs:

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

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

```sql theme={null}
-- ACL configuration
acl_authenticated: [Read]
```

### Row-Level Access

Subscription events are filtered by row-level access rules:

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

```json theme={null}
{
  "name": "posts",
  "table_name": "posts",
  "enable_subscriptions": true,
  "acl_authenticated": ["Read"],
  "read_access_rule": "_ROW_.owner = _USER_.id"
}
```

<Warning>
  Subscriptions only work on tables, not views. The underlying table must support SQLite's update hooks.
</Warning>

## JavaScript/TypeScript Client

### Using EventSource (SSE)

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

<Note>
  WebSocket support requires the `ws` feature to be enabled in your TrailBase build.
</Note>

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

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

```bash theme={null}
# 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](/api/records#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:

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

<ResponseField name="403" type="Forbidden">
  User doesn't have read access or subscriptions disabled
</ResponseField>

<ResponseField name="404" type="Not Found">
  Record or API not found
</ResponseField>

<ResponseField name="405" type="Method Not Allowed">
  API doesn't support subscriptions or uses a view instead of table
</ResponseField>
