Skip to main content

Database Subscriptions

Beta

This feature is in beta. Core behavior is stable and ready to try, but some APIs or configuration may still evolve 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.