Write Data to a Synced Realm - Swift SDK
On this page
- Overview
- Determining What Data Syncs
- App Services Configuration
- Client Data Model and Configuration
- What Data Syncs?
- Write to a Synced Realm
- Successful Writes
- Compensating Writes
- Compensating Write Error Information
- Writes that Don't Match the Query Subscription
- Writes That Don't Match Permissions
- Group Writes for Improved Performance
- Don't Write to a Synced Realm in an App Extension
- Crashes Related to Opening a Synced Realm in Multiple Processes
- Alternatives to Writing to a Synced Realm in an App Extension
- Pass Data On Disk
- Communicate Directly with the Backing Atlas Collection
Overview
When writing data to a synced realm using Flexible Sync, you can use the same APIs as writing to a local realm. However, there are some differences in behavior to keep in mind as you develop your application.
When you write to a synced realm, your write operations must match both of the following:
- The sync subscription query.
If your write operation doesn't match the query in the subscription, the write reverts with a non-fatal compensating write error (ErrorCompensatingWrite).
- The permissions in your App Services App.
If your try to write data that doesn't match the permissions expression, the write reverts with a non-fatal permission denied error. In the client, this shows as an error (ErrorCompensatingWrite). On the server, you can see more details about how the write was denied was by a write filter in the role.
To learn more about configuring permissions for your app, see Role-based Permissions and the Device Sync Permissions Guide in the App Services documentation.
Warning
Multiprocess Sync is Not Supported
Device Sync does not currently support opening or writing to a synced realm from more than one process. For more information, including suggested alternatives, refer to: Don't Write to a Synced Realm in an App Extension.
Determining What Data Syncs
The data that you can write to a synced realm is the intersection of your Device Sync configuration, your permissions, and the Flexible Sync subscription query that you use when you open the realm.
The examples on this page use the following configurations and models:
App Services Configuration
Device Sync is configured with the following queryable fields:
_id
(always included)complexity
ownerId
The App Services App has permissions configured to let users read and write only their own data:
{ "name": "owner-read-write", "apply_when": {}, "document_filters": { "read": { "ownerId": "%%user.id" }, "write": { "ownerId": "%%user.id" } }, "read": true, "write": true }
Client Data Model and Configuration
The examples on this page use the following object model:
class Item: Object { true) var _id: ObjectId (primaryKey: var ownerId: String var itemName: String var complexity: Int }
Using that object model, the synced realm configuration syncs objects that
match the subscription query where the complexity
property's value
is less than or equal to 4
:
let app = App(id: YOUR_APP_ID_HERE) do { let user = try await app.login(credentials: Credentials.anonymous) do { var flexSyncConfig = user.flexibleSyncConfiguration() flexSyncConfig.objectTypes = [Item.self] let realm = try await Realm(configuration: flexSyncConfig) let subscriptions = realm.subscriptions try await subscriptions.update { subscriptions.append( QuerySubscription<Item>(name: "simple-items") { $0.complexity <= 4 }) } print("Successfully opened realm: \(realm)") } catch { print("Failed to open realm: \(error.localizedDescription)") // handle error } } catch { fatalError("Login failed: \(error.localizedDescription)") }
What Data Syncs?
The subscription query combined with the permissions mean that the synced realm only syncs objects where:
The
ownerId
matches theuser.id
of the logged-in user (from the permissions)The
complexity
property's value is less than or equal to4
(from the subscription query)
Any object in the Atlas collection where the ownerId
does not match
the user.id
of the logged-in user, or the complexity
property's
value is greater than 4
, cannot sync to this realm.
Write to a Synced Realm
Writes to Flexible Sync realms may broadly fall into one of two categories:
Successful writes: The written object matches both the query subscription and the user's permissions. The object writes successfully to the realm, and syncs successfully to the App Services backend and other devices.
Compensating writes: When the written object does not match the subscription query, or where the user does not have sufficient permissions to perform the write, Realm reverts the illegal write.
Successful Writes
When the write matches both the permissions and the Flexible Sync subscription query in the client, the Realm Swift SDK can successfully write the object to the synced realm. This object syncs with the App Services backend when the device has a network connection.
// This write falls within the subscription query and complies // with the Device Sync permissions, so this write should succeed. do { let learnRealm = Item() learnRealm.ownerId = user.id learnRealm.itemName = "Learn Realm CRUD stuff" learnRealm.complexity = 3 try realm.write { realm.add(learnRealm) } } catch { print("Failed to write to realm: \(error.localizedDescription)") }
Compensating Writes
In some cases, a write that initially appears to succeed is actually an illegal write. In these cases, the object writes to the database, but when the database syncs to the backend, Realm reverts the write in a non-fatal error operation called a compensating write. Compensating writes can occur when:
Writes don't match the query subscription: The written object matches the user's permissions, but does not match the query subscription.
Writes don't match permissions: The written object matches the query subscription, but does not match the user's permissions.
In more detail, when you write data that is outside the bounds of a query subscription or does not match the user's permissions, the following occurs:
Because the client realm has no concept of "illegal" writes, the write initially succeeds until realm resolves the changeset with the App Services backend.
Upon sync, the server applies the rules and permissions. The server determines that the user does not have authorization to perform the write.
The server sends a revert operation, called a "compensating write", back to the client.
The client's realm reverts the illegal write operation.
Any client-side writes to a given object between an illegal write to that object and the corresponding compensating write will be lost.
In practice, this may look like an object being written to the realm, and then disappearing after the server sends the compensating write back to the client.
To learn more about permission denied errors, compensating write errors and other Device Sync error types, refer to Sync Errors in the App Services documentation.
The App Services logs contain more information about why a compensating write error occurs.
Compensating Write Error Information
New in version 10.37.0.
You can get additional information in the client about why a compensating
write occurs. The Swift SDK exposes a compensatingWriteInfo field on a
SyncError whose code is .writeRejected
. You can access this information
through the Sync error handler.
This field contains an array of RLMCompensatingWriteInfo objects, which provide:
The
objectType
of the object the client attempted to writeThe
primaryKey
of the specific objectThe
reason
for the compensating write error
This information is the same information you can find in the App Services logs. It is exposed on the client for convenience and debugging purposes.
Example
This error handler shows an example of how you might log information about compensating write errors:
myApp.syncManager.errorHandler = { syncError, session in if let thisError = syncError as? SyncError { switch thisError.code { // ... additional SyncError.code cases ... case .writeRejected: if let compensatingWriteErrorInfo = thisError.compensatingWriteInfo { for anError in compensatingWriteErrorInfo { print("A write was rejected with a compensating write error") print("The write to object type: \(anError.objectType)") print("With primary key of: \(anError.primaryKey)") print("Was rejected because: \(anError.reason)") } } } } }
The example error handler above produces this output when a compensating write error occurs:
A write was rejected with a compensating write error The write to object type: Optional("Item") With primary key of: objectId(641382026852d9220b2e2bbf) Was rejected because: Optional("write to \"641382026852d9220b2e2bbf\" in table \"Item\" not allowed; object is outside of the current query view")
The
Optional("Item")
in this message is anItem
object used in the Swift Template App.The primary key is the
objectId
of the specific object the client attempted to write.The
table \"Item"\
refers to the Atlas collection where this object would sync.The reason
object is outside of the current query view
in this example is because the query subscription was set to require the object'sisComplete
property to betrue
, and the client attempted to write an object where this property wasfalse
.
Writes that Don't Match the Query Subscription
You can only write objects to a Flexible Sync realm if they match the subscription query. If you perform a write that does not match the subscription query, Realm initially writes the object, but then performs a compensating write. This is a non-fatal operation that reverts an illegal write that does not match the subscription query.
In practice, this may look like the write succeeding, but then the object "disappears" when Realm syncs with the App Services backend and performs the compensating write.
If you want to write an object that does not match the query subscription, you must open a different realm where the object matches the query subscription. Alternately, you could write the object to a non-synced realm that does not enforce permissions or subscription queries.
Code Example
Given the configuration for the Flexible Sync realm above, attempting to write this object does not match the query subscription:
do { let fixTheBug = Item() fixTheBug.ownerId = user.id fixTheBug.itemName = "Fix the bug with the failing method" // The complexity of this item is `7`. This is outside the bounds // of the subscription query, so this write triggers a compensating write. fixTheBug.complexity = 7 try realm.write { realm.add(fixTheBug) } } catch { print("Failed to write to realm: \(error.localizedDescription)") }
Client Error
The error message in the client-side logs in this scenario is:
Sync: Connection[1]: Session[1]: Received: ERROR "Client attempted a write that is outside of permissions or query filters; it has been reverted" (error_code=231, try_again=true, error_action=Warning)
App Services Error
The error message in the App Services logs in this scenario is:
"FlexibleSync_Item": { "63bdfc40f16be7b1e8c7e4b7": "write to \"63bdfc40f16be7b1e8c7e4b7\" in table \"FlexibleSync_Item\" not allowed; object is outside of the current query view" }
Writes That Don't Match Permissions
Attempting to write to the client can also trigger a compensating write error when the object does not match the user's server-side write permissions.
On the client, this type of write behaves the same as a write that doesn't match the query subscription. In practice, this may look like the write succeeding, but then the object "disappears" when Realm syncs with the App Services backend and performs the compensating write.
Code Example
Given the permissions in the Device Sync Configuration detailed above,
attempting to write an object where the ownerId
property does not match
the user.id
of the logged-in user is not a legal write:
do { let itemWithWrongOwner = Item() // The `ownerId` of this item does not match the `user.id` of the logged-in // user. The user does not have permissions to make this write, so // it triggers a compensating write. itemWithWrongOwner.ownerId = "This string does not match the user.id" itemWithWrongOwner.itemName = "Write code that generates a permission error" itemWithWrongOwner.complexity = 1 try realm.write { realm.add(itemWithWrongOwner) } } catch { print("Failed to write to realm: \(error.localizedDescription)") }
Client Error
The client error in this scenario is the same as when you attempt to write an object that is outside the query filter:
Sync: Connection[1]: Session[1]: Received: ERROR "Client attempted a write that is outside of permissions or query filters; it has been reverted" (error_code=231, try_again=true, error_action=Warning)
App Services Error
The error message in the App Services logs provides some additional information to help you determine that it is a permissions issue, and not a query subscription issue. In this example, the error message shows that the the object does not match the user's role:
"FlexibleSync_Item": { "63bdfc40f16be7b1e8c7e4b8": "write to \"63bdfc40f16be7b1e8c7e4b8\" in table \"FlexibleSync_Item\" was denied by write filter in role \"owner-read-write\"" }
Group Writes for Improved Performance
Every write transaction for a subscription set has a performance cost. If you need to make multiple updates to a Realm object during a session, consider keeping edited objects in memory until all changes are complete. This improves sync performance by only writing the complete and updated object to your realm instead of every change.
Don't Write to a Synced Realm in an App Extension
If you are developing an app that uses App Extensions, such as a Share Extension, avoid writing to a synced realm in that extension. Device Sync supports opening a synced realm in at most one process. In practice, this means that if your app uses a synced realm in an App Extension, it may crash intermittently.
Crashes Related to Opening a Synced Realm in Multiple Processes
If you attempt to open a synced realm in a Share Extension or other App
Extension, and that realm is not open in the main app, a write from a Share
Extension may succeed. However, if the synced realm is already open in the
main app, or is syncing data in the background, you may see a crash related
to Realm::MultiSyncAgents
. In this scenario, you may need to restart
the device.
Alternatives to Writing to a Synced Realm in an App Extension
If you need to read from or write to a synced realm from an App Extension, there are a few recommended alternatives:
Offline-first: pass data on disk to or from the main app
Always up-to-date: communicate directly with the backing Atlas collection across a network connection
Pass Data On Disk
If offline-first functionality is the most important consideration for your app, you can pass data on disk to or from your main app. You could copy objects to a non-synced realm and read and share it between apps in an App Group. Or you could use an on-disk queue to send the data to or from the main app and only write to the synced realm from there. Then, regardless of the device's network connectivity, information can be shared any time to or from the App Extension.
Communicate Directly with the Backing Atlas Collection
If having the information always up-to-date across all devices is the most important consideration for your app, you can read or write data directly to or from the backing Atlas collection across the network. Depending on your needs, you may want to use one of these tools to communicate directly with Atlas:
Query Atlas with the Realm Swift SDK MongoClient
Pass data to an App Services Function
Make HTTPS calls with the Data API
Then, any device that has a network connection is always getting the most up-to-date information, without waiting for the user to open your main app as in the option above.
This option does require your user's device to have a network connection when using the App Extension. As a fallback, you could check for a network connection. Then, use the on-disk option above in the event that the user's device lacks network connectivity.