Abrova
☰ Docs menu · Realtime listeners
Coming soon. Abrova Sync is coming soon. This page describes how it will work; it can’t be used yet.

Realtime Listeners

Subscribe to live updates on collections, documents, or query results in AbrovaSync. Callbacks fire instantly on every change. Available on Flutter, React Native, Web, Android, iOS, and Node.js.

Last updated: 27 Oct 2026

Realtime listeners are what make AbrovaSync feel "live": instead of polling for changes, your code registers a callback once and the server pushes new data every time the underlying record changes. Three flavors are supported — listen to a whole collection, a single document, or a filtered query — and they all share the same lifecycle (register → receive snapshots → cancel).

Collection Listener

Fires when any document in the collection is added, updated, or removed. Best for short feeds and small lists.

Document Listener

Fires when one specific document changes or is deleted. Best for detail screens.

Query Listener

Same callback shape, but scoped to a .where(...).orderBy(...) chain.

Always cancel listeners you don't need

A listener you never cancel keeps an open subscription on the server and continues firing your callback. On screens with their own lifecycle, attach the cancel to dispose() / onDestroy() / deinit.

Use names, not IDs.

Get your references with the database and collection names shown in the console, e.g. database('my-app').collection('users'). A listener on a reference built from a database or collection ID never receives updates, so always use the names.

What a listener receives

The first callback has the current data: the documents of the collection or query, or the document (null when it does not exist). After that, the callback runs on every change. A listener on a collection or query that has no documents yet starts with an empty list and receives the first document when it is written. A document listener receives only changes to its own document.

Realtime reconnects by itself after a dropped connection, and open listeners resume. On Android, iOS, Flutter and React Native, listeners also follow the signed-in user: after sign-in, sign-out or a switch to another user they start again for that user.

Listeners follow your security rules

A listener is checked like a query (or a single read) when it starts, and again whenever your rules change or a document they look up changes (for example, a user removed from a team) — SDK 0.3.0 and later. It receives only the changes the user may read. If it isn't allowed, or stops being allowed, the listener ends and its error callback gets a typed error: query_not_allowed (with the fields to filter on) or listener_revoked. After an error the listener receives nothing more. The error callback is optional on every platform. Pass one to see why a listener stopped:

dart
// Flutter
final registration = lists.where('teamId', QueryOperator.equal, teamId).listen(
  (docs) => render(docs),
  onError: (error) {
    if (error is AbrovaSyncListenerError && error.isQueryNotAllowed) {
      print('Filter on: ${error.fields}');
    } else if (error is AbrovaSyncListenerError && error.isListenerRevoked) {
      print('No longer allowed to read these lists');
    }
  },
);
typescript
// React Native
import { AbrovaSyncError } from '@abrova/sync-react-native';

const registration = lists.where('teamId', '==', teamId).listen(
  (docs) => render(docs),
  (error) => {
    if (error.code === AbrovaSyncError.QUERY_NOT_ALLOWED) {
      console.warn('Filter on:', error.fields);
    } else if (error.code === AbrovaSyncError.LISTENER_REVOKED) {
      console.warn('No longer allowed to read these lists');
    }
  },
);
typescript
// Web, Node.js
const stop = db.collection('lists').where('teamId', '==', teamId).onSnapshot(
  (docs) => render(docs),
  (error) => console.warn(error.message), // query_not_allowed, listener_revoked, …
);
kotlin
// Android
val registration = lists.where("teamId", QueryOperator.EQUAL, teamId).listen(
    callback = { docs -> render(docs) },
    onError = { error ->
        when (error) {
            is AbrovaSyncException.QueryNotAllowedException -> Log.w("Sync", "Filter on: ${error.fields}")
            is AbrovaSyncException.ListenerRevokedException -> Log.w("Sync", "No longer allowed")
            else -> Log.w("Sync", error.message ?: "listener stopped")
        }
    },
)
swift
// iOS
let registration = lists.query().where("teamId", .equal, teamId).listen(
    callback: { docs in render(docs) },
    onError: { error in
        if let error = error as? AbrovaSyncError, error.code == "query_not_allowed" {
            print("Filter on: \(error.fields)")
        }
    }
)

The same error callback works on listen() and listenDocument() (Android, iOS, Flutter, React Native) and on every onSnapshot() (Web, Node.js). On Web and Node.js a collection listener takes it as the third argument: collection.onSnapshot(callback, undefined, onError). See Platform Notes for the error types.

Listener updates carry the changed document and its version; they never say who made the change.

Flutter#

dart
// Listen to collection changes
final registration = users.listen((documents) {
  print('Collection updated! Count: ${documents.length}');
  for (final doc in documents) {
    print('  ${doc.id}: ${doc.data['name']}');
  }
});

// Listen to a single document
final docRegistration = users.listenDocument('document-id', (document) {
  if (document != null) {
    print('Document updated: ${document.data}');
  } else {
    print('Document was deleted');
  }
});

// Listen to query results
final queryRegistration = users
    .query()
    .where('status', QueryOperator.equal, 'online')
    .listen((documents) {
      print('Online users: ${documents.length}');
    });

// Stop listening when done
registration.remove();
docRegistration.remove();
queryRegistration.remove();

To follow the realtime connection, listen to the AbrovaSync.connectionStateChanges stream (true when connected). Any number of widgets can listen to it; cancel the subscription in dispose(). The onConnectionState setter still works but keeps one callback, so a second screen replaces the first one's.

dart
final connectionSub = AbrovaSync.connectionStateChanges.listen((connected) {
  print(connected ? 'Live' : 'Reconnecting...');
});

// In dispose():
connectionSub.cancel();

React Native#

typescript
// Listen to collection changes
const registration = users.listen((documents) => {
  console.log('Collection updated! Count:', documents.length);
  documents.forEach(doc => {
    console.log(`  ${doc.id}: ${doc.data.name}`);
  });
});

// Listen to a single document
const docRegistration = users.listenDocument('document-id', (document) => {
  if (document) {
    console.log('Document updated:', document.data);
  } else {
    console.log('Document was deleted');
  }
});

// Listen to query results
const queryRegistration = users
    .query()
    .where('status', '==', 'online')
    .listen((documents) => {
      console.log('Online users:', documents.length);
    });

// Stop listening when done
registration.remove();
docRegistration.remove();
queryRegistration.remove();

Web#

typescript
// Listen to collection changes
const unsubscribe = users.onSnapshot((documents) => {
  console.log('Collection updated! Count:', documents.length);
  documents.forEach(doc => {
    console.log(`  ${doc.id}: ${doc.data.name}`);
  });
});

// Listen to a single document
const userRef = users.document('document-id');
const unsubscribeDoc = userRef.onSnapshot((document) => {
  if (document) {
    console.log('Document updated:', document.data);
  } else {
    console.log('Document was deleted');
  }
});

// Listen to query results
const unsubscribeQuery = users
    .where('status', '==', 'online')
    .onSnapshot((documents) => {
      console.log('Online users:', documents.length);
    });

// Stop listening when done
unsubscribe();
unsubscribeDoc();
unsubscribeQuery();

Android#

kotlin
// Listen to collection changes
val registration = users.listen { documents ->
    Log.d("AbrovaSync", "Collection updated! Count: ${documents.size}")
    documents.forEach { doc ->
        Log.d("AbrovaSync", "  ${doc.id}: ${doc.data["name"]}")
    }
}

// Listen to a single document
val docRegistration = users.listenDocument("document-id") { document ->
    if (document != null) {
        Log.d("AbrovaSync", "Document updated: ${document.data}")
    } else {
        Log.d("AbrovaSync", "Document was deleted")
    }
}

// Listen to query results
val queryRegistration = users
    .query()
    .where("status", QueryOperator.EQUAL, "online")
    .listen { documents ->
        Log.d("AbrovaSync", "Online users: ${documents.size}")
    }

// Stop listening when done (e.g., in onDestroy)
registration.remove()
docRegistration.remove()
queryRegistration.remove()

iOS#

swift
// Listen to collection changes
let registration = users.listen { documents in
    print("Collection updated! Count: \(documents.count)")
    for doc in documents {
        print("  \(doc.id): \(doc.data["name"] ?? "")")
    }
}

// Listen to a single document
let docRegistration = users.listenDocument(documentId: "document-id") { document in
    if let document = document {
        print("Document updated: \(document.data)")
    } else {
        print("Document was deleted")
    }
}

// Listen to query results
let queryRegistration = users
    .query()
    .where("status", .equal, "online")
    .listen { documents in
        print("Online users: \(documents.count)")
    }

// Stop listening when done (e.g., in deinit)
registration.remove()
docRegistration.remove()
queryRegistration.remove()

Node.js#

Realtime is opt-in on the server SDK — set enableRealtime: true in the config. Otherwise the SDK runs in pure-request mode (smaller footprint, no persistent connection). When the server refuses realtime (for example a wrong server secret), every listener's error callback receives the error. disconnect() stops every listener.

typescript
// Note: Realtime requires enableRealtime: true in config
// const abrovaSync = new AbrovaSyncAdmin({
//   apiKey: '...',
//   serverSecret: '...',
//   enableRealtime: true,
// });

// Listen to collection changes (server-side)
const unsubscribe = users.onSnapshot((documents) => {
  console.log('Collection updated! Count:', documents.length);
  documents.forEach(doc => {
    console.log(`  ${doc.id}: ${doc.data.name}`);
  });
});

// Listen to a single document
const unsubscribeDoc = users.doc('document-id').onSnapshot((document) => {
  if (document) {
    console.log('Document updated:', document.data);
  } else {
    console.log('Document was deleted');
  }
});

// Listen to query results
const unsubscribeQuery = users
    .where('status', '==', 'online')
    .onSnapshot((documents) => {
      console.log('Online users:', documents.length);
    });

// Stop listening when done
unsubscribe();
unsubscribeDoc();
unsubscribeQuery();

REST API#

The REST API has no realtime channel, and AbrovaSync does not send webhooks for document changes. To react to changes from a backend, use the Node.js SDK (step 6 above) with enableRealtime: true; from any other HTTP client, poll the query endpoint (see Querying Data).

Next: keep working when the network drops — Offline Persistence.

Was this page helpful?
Report a problem