Platform Notes
Per-platform behavior notes for the Abrova Sync SDKs — Flutter and React Native wrap native, Web uses IndexedDB, Node.js is server-side, REST has no realtime channel — and the error types each SDK throws.
Behavior, authentication, and capability differences between each AbrovaSync SDK and the REST API.
Per-platform behavior#
| Platform | Notes |
|---|---|
| Flutter & React Native | Wrap the native Android and iOS SDKs — best performance with a consistent API across both platforms. |
| Web SDK | Built-in offline support via IndexedDB. Offline writes sync automatically when the connection returns. |
| Native Android & iOS | Both async/await and callback-based APIs. Choose the style that fits your app architecture. |
| Node.js / Backend SDK | Server-side SDK. Requires both apiKey and serverSecret. Once a database has rules they apply to it as to any client (auth.uid is the userId you configure); a database with no rules is open to it, while client SDKs are locked out. Realtime is opt-in (enableRealtime). Batches are atomic, as on every SDK. |
| REST API | Direct HTTP access from any language. There is no realtime channel or change webhook: poll, or use the Node.js SDK with enableRealtime. |
Live updates use an encrypted connection on every platform (Android 0.2.5+, iOS 0.2.4+, Flutter and React Native 0.2.5+, Web, Node.js 0.2.2+).
Never use the server secret in client-side code (mobile/web apps). Anything shipped in an app should be treated as public, and the server secret can mint a user token for anyone in your project. Client apps use the API key only, plus a short-lived user token your backend mints for them — see Security Rules.
Errors#
When a call fails, every SDK throws (or rejects with) an error that carries the HTTP status of the server's answer as statusCode. Use statusCode to tell a refusal from a connection problem:
| statusCode | Meaning |
|---|---|
| 403 | Your security rules refused the request. |
| 404 | The document does not exist. |
| 400 / 422 | The server does not accept the request (for example invalid data). |
| 401 | The user token is missing, expired or not valid. |
| null / undefined | The server could not be reached (no network, timeout). |
With offline persistence on, a call that cannot reach the server is answered from the cache or queued instead of failing. A refusal by the server is always thrown.
Errors from your security rules also carry a code:
query_not_allowed: the rules do not allow a query or a listener, because it could return documents the user may not read.fieldsnames the fields to filter on. See Queries and your security rules.listener_revoked: a listener was stopped because the rules no longer allow it (for example, the user was removed from a team). It receives nothing more and is not restarted.permission_denied: the rules do not allow reading what a listener listens to (Flutter, React Native, Web).
| Platform | Error type | Fields |
|---|---|---|
| Android | AbrovaSyncException.NetworkException, with the subclasses QueryNotAllowedException and ListenerRevokedException | statusCode, code, fields (QueryNotAllowedException) |
| iOS | AbrovaSyncError, with the cases .queryNotAllowed(_, fields:) and .listenerRevoked | statusCode, code, fields |
| Flutter | AbrovaSyncError for calls; AbrovaSyncListenerError for listener errors | code, statusCode, fields; isPermissionDenied (AbrovaSyncError); isQueryNotAllowed, isListenerRevoked (AbrovaSyncListenerError) |
| React Native | AbrovaSyncError | code, statusCode, fields |
| Web | AbrovaSyncError | code, reason, statusCode (also status), fields |
| Node.js | AbrovaSyncError, with the subclasses QueryNotAllowedError and ListenerRevokedError | code, statusCode (also status), serverCode, fields (QueryNotAllowedError) |
// Flutter
try {
final open = await todos.where('teamId', QueryOperator.equal, teamId).get();
} on AbrovaSyncError catch (e) {
if (e.code == AbrovaSyncErrorCode.queryNotAllowed) {
print('Filter on: ${e.fields}');
} else if (e.isPermissionDenied) {
print('Refused by the security rules');
} else if (e.statusCode == null) {
print('Could not reach the server');
}
}// React Native
import { AbrovaSyncError } from '@abrova/sync-react-native';
try {
const open = await todos.where('teamId', '==', teamId).get();
} catch (e) {
if (e instanceof AbrovaSyncError && e.code === AbrovaSyncError.QUERY_NOT_ALLOWED) {
console.log('Filter on:', e.fields);
} else if (e instanceof AbrovaSyncError && e.statusCode === 403) {
console.log('Refused by the security rules');
}
}// Web
import { AbrovaSyncError } from '@abrova/sync-web';
try {
const open = await todos.where('teamId', '==', teamId).get();
} catch (e) {
if (e instanceof AbrovaSyncError && e.reason === 'query_not_allowed') {
console.log('Filter on:', e.fields);
} else if (e instanceof AbrovaSyncError && e.statusCode === 403) {
console.log('Refused by the security rules');
} else if (e instanceof AbrovaSyncError && e.statusCode === undefined) {
console.log('Could not reach the server');
}
}// Node.js
import { AbrovaSyncError, QueryNotAllowedError } from '@abrova/sync-node';
try {
const open = await todos.where('teamId', '==', teamId).get();
} catch (e) {
if (e instanceof QueryNotAllowedError) {
console.log('Filter on:', e.fields);
} else if (e instanceof AbrovaSyncError && e.statusCode === 403) {
console.log('Refused by the security rules');
} else if (e instanceof AbrovaSyncError && e.statusCode === undefined) {
console.log('Could not reach the server');
}
}// Android
try {
val open = todos.where("teamId", QueryOperator.EQUAL, teamId).get()
} catch (e: AbrovaSyncException.QueryNotAllowedException) {
Log.w("Todos", "Filter on: ${e.fields}")
} catch (e: AbrovaSyncException.NetworkException) {
when (e.statusCode) {
403 -> Log.w("Todos", "Refused by the security rules")
null -> Log.w("Todos", "Could not reach the server")
else -> Log.w("Todos", "Failed: ${e.message}")
}
}// iOS
do {
let open = try await todos.query().where("teamId", .equal, teamId).get()
} catch AbrovaSyncError.queryNotAllowed(_, let fields) {
print("Filter on: \(fields)")
} catch let error as AbrovaSyncError {
switch error.statusCode {
case 403: print("Refused by the security rules")
case nil: print("Could not reach the server")
default: print("Failed: \(error.localizedDescription)")
}
}Listener errors arrive in the listener's error callback instead of being thrown. See Realtime Listeners.