Skip to main content

Database Subscriptions

Beta

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

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();

Document Subscription

Subscribe to changes on a single document:

const unsubscribe = client.db('app').table('posts').doc('post-id').onSnapshot((event) => {
console.log('Document changed:', event.data);
});

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.

Admin SDKs

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:

TypeDescriptionevent.data
addedNew document createdFull document
modifiedExisting document updatedFull document (after update)
removedDocument deletednull

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_revoked event: 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"]
}
FieldDescription
revokedChannelsList 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, revokedChannels is 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,
});
}
Subscriptions vs Room

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.