CRUD Operations
Create, read, update, set, and delete documents in Abrova Sync collections across Flutter, React Native, Web, Android, iOS, Node.js, and REST API.
Every piece of data in AbrovaSync lives inside a document — a JSON object with a unique ID — and documents live inside named collections, which sit inside a database. CRUD is therefore always one of four moves: add a new document, read an existing one, change part of it, or remove it.
Database
Top-level container. Created in the Abrova Console — you reference it from code by name (unique within a project).
Collection
A named group of documents. Created on first write — no migrations.
Document
A JSON object with a unique ID. Add nested objects, arrays, numbers, anything JSON-serializable.
The five primitives
- 1add() — create a new document with an auto-generated ID.
- 2get() — read a document by ID, or fetch every document in the collection.
- 3update() — merge fields into an existing document (partial update).
- 4set() — replace a document's data entirely.
- 5delete() — remove a document.
Databases are created and managed via the Abrova Console, not from SDKs. Create your databases in the console, then use the SDK to interact with them using the database name shown in the console, e.g. database('my-app').collection('users'). The API key picks the project, and a database name is unique within a project. Don't pass database or collection IDs: live listeners need the names.
Flutter#
// Get database and collection references
final db = AbrovaSync.database('my-app');
final users = db.collection('users');
// CREATE - Add a new document
final newUser = await users.add({
'name': 'John Doe',
'email': 'john@example.com',
'age': 28,
'createdAt': DateTime.now().toIso8601String(),
});
print('Created user with ID: ${newUser.id}');
// READ - Get a single document
final user = await users.get('document-id');
print('User name: ${user?.data['name']}');
// READ - Get all documents
final allUsers = await users.getAll();
for (final user in allUsers) {
print('${user.id}: ${user.data['name']}');
}
// UPDATE - Partial update
await users.update('document-id', {
'age': 29,
'updatedAt': DateTime.now().toIso8601String(),
});
// SET - Replace entire document
await users.set('document-id', {
'name': 'John Updated',
'email': 'john.new@example.com',
'age': 29,
});
// DELETE - Remove a document
await users.delete('document-id');React Native#
// Get database and collection references
const db = AbrovaSync.database('my-app');
const users = db.collection('users');
// CREATE - Add a new document
const newUser = await users.add({
name: 'John Doe',
email: 'john@example.com',
age: 28,
createdAt: new Date().toISOString(),
});
console.log('Created user with ID:', newUser.id);
// READ - Get a single document
const user = await users.get('document-id');
console.log('User name:', user?.data.name);
// READ - Get all documents
const allUsers = await users.getAll();
allUsers.forEach(user => {
console.log(`${user.id}: ${user.data.name}`);
});
// UPDATE - Partial update
await users.update('document-id', {
age: 29,
updatedAt: new Date().toISOString(),
});
// SET - Replace entire document
await users.set('document-id', {
name: 'John Updated',
email: 'john.new@example.com',
age: 29,
});
// DELETE - Remove a document
await users.delete('document-id');Web#
The Web SDK uses document references (users.document('id')) instead of passing the ID into every call. Pick whichever style your team prefers — the underlying API is identical.
// Get database and collection references
const db = abrovaSync.database('my-app');
const users = db.collection('users');
// CREATE - Add a new document
const newUser = await users.add({
name: 'John Doe',
email: 'john@example.com',
age: 28,
createdAt: new Date().toISOString(),
});
console.log('Created user with ID:', newUser.id);
// READ - Get a single document
const userRef = users.document('document-id');
const user = await userRef.get();
console.log('User name:', user?.data.name);
// READ - Get all documents
const allUsers = await users.get();
allUsers.forEach(user => {
console.log(`${user.id}: ${user.data.name}`);
});
// UPDATE - Partial update
await userRef.update({
age: 29,
updatedAt: new Date().toISOString(),
});
// SET - Replace entire document
await userRef.set({
name: 'John Updated',
email: 'john.new@example.com',
age: 29,
});
// DELETE - Remove a document
await userRef.delete();Android#
All CRUD operations are suspend functions — call them from a coroutine scope (lifecycleScope, viewModelScope, etc.).
// Get database and collection references
val db = AbrovaSync.getInstance().database("my-app")
val users = db.collection("users")
// CREATE - Add a new document (using coroutines)
lifecycleScope.launch {
val newUser = users.add(mapOf(
"name" to "John Doe",
"email" to "john@example.com",
"age" to 28,
"createdAt" to System.currentTimeMillis()
))
Log.d("AbrovaSync", "Created user with ID: ${newUser.id}")
}
// READ - Get a single document
lifecycleScope.launch {
val user = users.get("document-id")
Log.d("AbrovaSync", "User name: ${user?.data?.get("name")}")
}
// READ - Get all documents
lifecycleScope.launch {
val allUsers = users.getAll()
allUsers.forEach { user ->
Log.d("AbrovaSync", "${user.id}: ${user.data["name"]}")
}
}
// UPDATE - Partial update
lifecycleScope.launch {
users.update("document-id", mapOf(
"age" to 29,
"updatedAt" to System.currentTimeMillis()
))
}
// SET - Replace entire document
lifecycleScope.launch {
users.set("document-id", mapOf(
"name" to "John Updated",
"email" to "john.new@example.com",
"age" to 29
))
}
// DELETE - Remove a document
lifecycleScope.launch {
users.delete("document-id")
}iOS#
iOS uses Swift's async/await for every operation. Wrap calls in a Task { } block when invoked from a synchronous context like SwiftUI views.
// Get database and collection references
let db = AbrovaSync.shared.database("my-app")
let users = db.collection("users")
// CREATE - Add a new document
Task {
let newUser = try await users.add(data: [
"name": "John Doe",
"email": "john@example.com",
"age": 28,
"createdAt": Date().ISO8601Format()
])
print("Created user with ID: \(newUser.id)")
}
// READ - Get a single document
Task {
if let user = try await users.get(documentId: "document-id") {
print("User name: \(user.data["name"] ?? "")")
}
}
// READ - Get all documents
Task {
let allUsers = try await users.getAll()
for user in allUsers {
print("\(user.id): \(user.data["name"] ?? "")")
}
}
// UPDATE - Partial update
Task {
try await users.update(documentId: "document-id", data: [
"age": 29,
"updatedAt": Date().ISO8601Format()
])
}
// SET - Replace entire document
Task {
try await users.set(documentId: "document-id", data: [
"name": "John Updated",
"email": "john.new@example.com",
"age": 29
])
}
// DELETE - Remove a document
Task {
try await users.delete(documentId: "document-id")
}Node.js#
The Admin SDK exposes both the collection-level shortcut (users.add(...)) and the document-reference style (users.doc('id').update(...)). The latter is handy when you need to chain multiple writes on the same doc.
// Get database and collection references
const db = abrovaSync.database('my-app');
const users = db.collection('users');
// CREATE - Add a new document
const newUser = await users.add({
name: 'John Doe',
email: 'john@example.com',
age: 28,
createdAt: new Date().toISOString(),
});
console.log('Created user with ID:', newUser.id);
// READ - Get a single document
const user = await users.get('document-id');
console.log('User name:', user?.data.name);
// READ - Get all documents
const allUsers = await users.getAll();
allUsers.forEach(user => {
console.log(`${user.id}: ${user.data.name}`);
});
// UPDATE - Partial update (merge with existing)
await users.doc('document-id').update({
age: 29,
updatedAt: new Date().toISOString(),
});
// SET - Replace entire document
await users.doc('document-id').set({
name: 'John Updated',
email: 'john.new@example.com',
age: 29,
});
// DELETE - Remove a document
await users.doc('document-id').delete();REST API#
Each primitive maps to a standard HTTP verb. POST for create, GET for read, PATCH for partial update, PUT for replace (it creates the document if the ID does not exist yet), DELETE for remove. API-key endpoints live under /databases/{databaseName}/collections/{collectionName}/documents/sdk, and every response is wrapped as { "data": ... }. To act as a signed-in user instead of your backend, send x-user-token in place of x-server-secret.
# Base URL: https://sync.abrova.ir
# CREATE - Add a new document (SDK auth)
curl -X POST "https://sync.abrova.ir/databases/{databaseName}/collections/{collectionName}/documents/sdk" \
-H "x-api-key: YOUR_PROJECT_API_KEY" \
-H "x-server-secret: YOUR_SERVER_SECRET" \
-H "Content-Type: application/json" \
-d '{
"data": {
"name": "John Doe",
"email": "john@example.com",
"age": 28
}
}'
# READ - Get a single document
curl -X GET "https://sync.abrova.ir/databases/{databaseName}/collections/{collectionName}/documents/sdk/{documentId}" \
-H "x-api-key: YOUR_PROJECT_API_KEY" \
-H "x-server-secret: YOUR_SERVER_SECRET"
# READ - Get all documents in a collection
curl -X GET "https://sync.abrova.ir/databases/{databaseName}/collections/{collectionName}/documents/sdk" \
-H "x-api-key: YOUR_PROJECT_API_KEY" \
-H "x-server-secret: YOUR_SERVER_SECRET"
# UPDATE - Partial update
curl -X PATCH "https://sync.abrova.ir/databases/{databaseName}/collections/{collectionName}/documents/sdk/{documentId}" \
-H "x-api-key: YOUR_PROJECT_API_KEY" \
-H "x-server-secret: YOUR_SERVER_SECRET" \
-H "Content-Type: application/json" \
-d '{
"data": {
"age": 29,
"updatedAt": "2024-01-15T10:30:00Z"
}
}'
# SET - Replace entire document
curl -X PUT "https://sync.abrova.ir/databases/{databaseName}/collections/{collectionName}/documents/sdk/{documentId}" \
-H "x-api-key: YOUR_PROJECT_API_KEY" \
-H "x-server-secret: YOUR_SERVER_SECRET" \
-H "Content-Type: application/json" \
-d '{
"data": {
"name": "John Updated",
"email": "john.new@example.com",
"age": 29
},
"merge": false
}'
# DELETE - Remove a document
curl -X DELETE "https://sync.abrova.ir/databases/{databaseName}/collections/{collectionName}/documents/sdk/{documentId}" \
-H "x-api-key: YOUR_PROJECT_API_KEY" \
-H "x-server-secret: YOUR_SERVER_SECRET""merge": false replaces the document, "merge": true merges the fields into it. A PUT without merge merges, as it always did — send the flag so the call says what it means.
Merge instead of replace#
set() replaces the whole document (SDK 0.3.0 and later). To write only some fields and keep the rest — creating the document if it doesn't exist — pass the merge option:
// Flutter
await users.set('document-id', {'age': 29}, merge: true);// React Native
await users.set('document-id', { age: 29 }, { merge: true });
// Web
await users.document('document-id').set({ age: 29 }, { merge: true });
// Node.js
await users.doc('document-id').set({ age: 29 }, { merge: true });// Android
users.set("document-id", mapOf("age" to 29), merge = true)// iOS try await users.set(documentId: "document-id", data: ["age": 29], merge: true)
update() also changes only the fields you pass, but the document must already exist.
In SDKs before 0.3.0, set() merged. If your app relied on that, add the merge option where it calls set(). See Upgrading to 0.3.0 for the other changes.
Deleting and errors#
Deleting a document that does not exist succeeds, so you can call delete() again safely, for example when a retry follows a delete that already went through.
When the server refuses a read or a write, the call throws (or rejects) with an error whose statusCode says why: 403 when your security rules refused it, 404 when the document does not exist (for example update() of a missing document), and no status when the server could not be reached. get() of a single document that does not exist returns null instead of throwing. On Flutter, catch AbrovaSyncError:
try {
await users.update('document-id', {'age': 29});
} on AbrovaSyncError catch (e) {
if (e.isPermissionDenied) {
print('Not allowed by the security rules');
}
}The error types for every platform are in Platform Notes.
Next: filter and sort with Querying Data.