Database Subscriptions
This feature is in beta. Core behavior is stable, but some APIs or configuration may change before general availability.
Listen to real-time database changes with onSnapshot. Use client.db(namespace).table(name) to access the correct DB block.
Table Subscription
- JavaScript
- Dart/Flutter
- Swift
- Kotlin
- Java
- C#
- C++
const unsubscribe = client.db('app').table('posts').onSnapshot((event) => {
if (event.type === 'added') {
console.log('New post:', event.data);
} else if (event.type === 'modified') {
console.log('Updated:', event.data);
} else if (event.type === 'removed') {
console.log('Deleted:', event.docId);
}
});
// Stop listening
unsubscribe();
final subscription = client.db('app').table('posts').onSnapshot((event) {
if (event.type == ChangeType.added) {
print('New post: ${event.data}');
}
});
// Stop listening
subscription.cancel();
let subscription = client.db("app").table("posts").onSnapshot { event in
switch event.type {
case .added: print("New post: \(event.data)")
case .modified: print("Updated: \(event.data)")
case .removed: print("Deleted: \(event.docId)")
}
}
subscription.cancel()
val subscription = client.db("app").table("posts").onSnapshot { event ->
when (event.type) {
"added" -> println("New: ${event.data}")
"modified" -> println("Updated: ${event.data}")
"removed" -> println("Deleted: ${event.docId}")
}
}
subscription.cancel()
Subscription sub = client.db("app").table("posts").onSnapshot(event -> {
System.out.println(event.getType() + ": " + event.getData());
});
// Later: sub.cancel();
var sub = client.Db("app").Table("posts").OnSnapshot(change => {
if (change.Type == "added")
Console.WriteLine($"New post: {change.Data}");
});
// Stop listening
sub.Cancel();
int subId = client.db().onSnapshot("posts", [](const eb::DbChange& change) {
if (change.changeType == "added")
std::cout << "New post: " << change.dataJson << std::endl;
});
// Stop listening
client.db().unsubscribe(subId);
Document Subscription
Subscribe to changes on a single document:
- JavaScript
- Dart/Flutter
- Swift
- Kotlin
- Java
- C#
- C++
const unsubscribe = client.db('app').table('posts').doc('post-id').onSnapshot((event) => {
console.log('Document changed:', event.data);
});
final subscription = client.db('app').table('posts').doc('post-id').onSnapshot((event) {
print('Document changed: ${event.data}');
});
let subscription = client.db("app").table("posts").doc("post-id").onSnapshot { event in
print("Document changed: \(event.data)")
}
val subscription = client.db("app").table("posts").doc("post-id").onSnapshot { event ->
println("Document changed: ${event.data}")
}
Subscription sub = client.db("app").table("posts").doc("post-id").onSnapshot(event -> {
System.out.println("Document changed: " + event.getData());
});
var sub = client.Db("app").Table("posts").Doc("post-id").OnSnapshot(change => {
Console.WriteLine($"Document changed: {change.Data}");
});
int subId = client.db().onDocSnapshot("posts", "post-id", [](const eb::DbChange& change) {
std::cout << "Document changed: " << change.dataJson << std::endl;
});
When a change occurs, both the table-level subscription and the document-level subscription receive the event simultaneously (dual propagation).
Filtered Subscriptions
Filter events using where(). EdgeBase supports two filtering modes:
Client-Side Filtering (Default)
The SDK receives all events and filters locally. No additional configuration needed:
const unsubscribe = client.db('app').table('posts')
.where('status', '==', 'published')
.onSnapshot((event) => {
// Only receives events for published posts
});
Server-Side Filtering
For high-traffic tables, enable server-side filtering so the server only sends matching events. This reduces bandwidth and client-side processing:
const unsubscribe = client.db('app').table('posts')
.where('status', '==', 'published')
.onSnapshot((event) => {
// Server only sends events where status == 'published'
}, { serverFilter: true });
Server-side filters support AND conditions, OR conditions, 8 comparison operators, and runtime updates. See Server-Side Filters for the full guide.
Database subscriptions are only available in client SDKs (JavaScript, Dart, Swift, Kotlin, C#, C++). Server-only Admin SDKs do not support onSnapshot. Use server-side broadcast instead.
Event Types
Every onSnapshot callback receives an event with one of three types:
| Type | Description | event.data |
|---|---|---|
added | New document created | Full document |
modified | Existing document updated | Full document (after update) |
removed | Document deleted | null |
Authentication
WebSocket connections require JWT authentication. The SDK handles this automatically — after connecting, it sends an auth message before any subscriptions. See Architecture for protocol details.
Key behaviors:
- Timeout: 5000ms default
- Token refresh: The SDK automatically sends refreshed tokens. The server re-evaluates all subscriptions and revokes any that are no longer authorized.
subscription_revokedevent: Listen globally to handle permission changes:
client.databaseLive.onError((error) => {
if (error.code === 'SUBSCRIPTION_REVOKED') {
console.warn('Subscription revoked:', error.message);
}
});
Token Refresh and Revoked Channels
When a client's auth token is refreshed on a long-lived WebSocket connection, the server re-evaluates channel access. The response includes any channels the client lost access to:
{
"type": "auth_refreshed",
"userId": "user-123",
"revokedChannels": ["db:private-table"]
}
| Field | Description |
|---|---|
revokedChannels | List of channels the client lost access to after token refresh |
- The client should handle this by removing subscriptions for revoked channels
- This occurs when user roles or permissions change while the client is connected
- If no channels are revoked,
revokedChannelsis an empty array - If the refresh fails, existing auth is preserved and a non-fatal error is returned
Batch Changes
When many changes occur simultaneously (e.g., bulk operations), the server batches them into a single batch_changes message instead of individual events. The batch threshold is 10 changes by default.
Your onSnapshot callback receives each change individually — the SDK unpacks batch events automatically.
High-Frequency Update Pattern
For scenarios with very frequent updates (e.g., real-time analytics), consider debouncing your UI updates:
let pending: SnapshotEvent[] = [];
client.db('app').table('metrics').onSnapshot((event) => {
pending.push(event);
requestAnimationFrame(() => {
if (pending.length > 0) {
renderUpdates(pending);
pending = [];
}
});
});
Type-Safe Subscriptions
Use the generic parameter on onSnapshot<T>() with types generated by npx edgebase typegen:
import type { Post } from './edgebase.d.ts';
// Typed subscription — change.data is Post | null
const unsub = client.db('app').table('posts').onSnapshot<Post>((change) => {
console.log(change.data?.title); // TypeScript autocomplete
});
Access Rules
Database subscriptions reuse the table's existing read access rule. No additional configuration is needed.
export default defineConfig({
databases: {
app: {
tables: {
posts: {
access: {
read(auth, doc) { return auth !== null }, // also used for onSnapshot
},
},
},
},
},
});
When a client calls .onSnapshot(), the server evaluates the read rule once at subscribe time. After subscription, every database change event is delivered to the subscriber. Use server-side filters for per-event filtering.
Connection Management
- Auto-reconnect — Reconnects automatically on disconnection with exponential backoff
- Namespace-aware — Use
client.db(namespace, id?)to route subscriptions to the correct DB block - Tab token sync — Auth/token state is synchronized across browser tabs
- Hibernation recovery — Idle WebSocket connections cost $0 via Cloudflare's Hibernation API. On wake-up, the SDK automatically re-registers filters and resumes subscriptions.
Server-Side Broadcast
Admin SDKs can broadcast messages to database subscription channels via the REST API. This uses Service Key authentication and is useful for server-originated notifications.
POST /api/db/broadcast
Headers:
X-EdgeBase-Service-Key: <your-service-key>(required)Content-Type: application/json
Body:
{
"channel": "chat-room",
"event": "message",
"payload": { "text": "System announcement" }
}
Inside App Functions, use context.admin.broadcast():
export default async function onPostCreate(doc, context) {
await context.admin.broadcast('updates', 'new-post', {
id: doc.id,
title: doc.data.title,
});
}
Database subscriptions deliver database change events — there's no server-side state management. If you need server-authoritative state (game logic, collaborative editing, etc.), use Room instead. For presence tracking and client-to-client messaging, use Room Members and Room Signals.