Designing Schemas and Permissions for Realm Flexible Sync: A Practical Guide
Learn how to design Realm schemas and App Services permissions for Flexible Sync so offline‑first apps sync reliably and avoid costly resets.
04 Nov 2025, 00:34 UTC

The problem: sync‑ready data must be planned before you write code
When you add Atlas Device Sync to a mobile app, the local Realm file becomes the source of truth while offline. If you wait until after you’ve written UI and business logic to think about primary keys, indexed fields, or who can read/write which objects, you’ll often hit sync errors, unexpected client resets, or permission denials that force rewrites.
Thesis: a clear schema and permission model up front prevents most sync headaches
By defining every synced object with a stable primary key, declaring which fields need indexes for queries, and expressing access rules in Atlas App Services before you enable Flexible Sync, you get:
- Deterministic replication of the data each device actually needs.
- Automatic, conflict‑free merges when two devices edit different fields of the same object.
- A predictable path for handling schema changes (client reset) because the server already knows what fields exist.
Defining synced objects
Every object that will be synchronized must have a primary key that never changes after creation. In Realm Swift, this looks like:
class TodoItem: Object {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var ownerId: String // the App Services user id
@Persisted var title: String
@Persisted var isComplete: Bool = false
}
The _id field provides a globally unique identifier. Adding an index on ownerId speeds up the subscription query that limits each user to their own items:
let config = Realm.Configuration(
syncConfiguration: SyncConfiguration(user: user, realmURL: realmURL),
objectTypes: [TodoItem.self]
)
Flexible Sync subscriptions and App Services permission rules
With Flexible Sync, the client declares a query that tells the server which subset of data to replicate. For a todo app where each user should only see their own items, the subscription is:
let subscription = user.subscriptions.update {
$0.add(TodoItem.self, where: { $0.ownerId == user.id })
}
On the server side, you create a role in Atlas App Services that evaluates the same condition. A minimal rule JSON for the TodoItem collection might be:
{
"roles": [
{
"name": "todo-owner",
"applyWhen": {"%user.id": "$$root.ownerId"},
"permissions": [
{
"actions": ["read", "write"],
"applyWhen": {"%user.id": "$$root.ownerId"}
}
]
}
]
}
This rule says: a user can read or write a TodoItem only if the ownerId field matches their own user id. Because the rule is evaluated server‑side, the client never receives data it isn’t allowed to see.
Worked example: end‑to‑end flow
- Define
TodoItemwith a primary key andownerIdfield. - Add an index on
ownerId(viaRealm.Configuration.schemaor the SDK’s index API). - Create the App Services role shown above and link it to the
TodoItemcollection. - In the app, log in a user, open a synced Realm with the configuration, and add the subscription that filters by
ownerId. - Make a local write (e.g., create a new
TodoItemwithownerId = user.id) while offline. - Reconnect; the change uploads to Atlas, merges with any concurrent changes, and downloads any new items that match the subscription.
You can verify the flow by:
- Checking the Atlas Data Explorer for the inserted document after reconnection.
- Turning on airplane mode, creating items, then disabling airplane mode and confirming the items appear in the cloud and on other logged‑in devices.
Trade‑offs and limitations
Even with a good design, two practical concerns remain:
- Schema evolution. Changing the object model (adding/removing fields, altering types) requires a client reset unless you use a non‑breaking additive change. You must implement a client‑reset handler that either discards unsynced changes or attempts to migrate them manually.
- Local file growth. The Realm file retains sync history until compacted. Over time, especially with many writes, the file can grow beyond expected bounds. Periodically calling
Realm.compact(config:)when the file exceeds a size threshold keeps storage usage predictable.
To check that your client‑reset handler works, you can simulate a reset in App Services by toggling the sync state off and on for the app, then observe whether your handler is called and whether the app recovers to a usable state.
Actionable closing
Start small: pick a single entity (like the TodoItem above), give it a stable primary key, add any fields you need to query on, write the matching Flexible Sync subscription, and enforce the same condition in an App Services role. Test the offline‑write → reconnect cycle and verify that conflicts resolve automatically. Once that loop is reliable, expand to additional entities, remembering that each new schema change will need a client‑reset plan. With the schema and permission model settled early, the rest of your app can focus on features rather than sync surprises.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.