Why a device needs a scope
In a local-first app the data a person works with lives on their device, not only on a server. That raises a question a cloud app can mostly ignore: which data should each device hold? No phone should carry every record in the system. It wastes storage and bandwidth, it slows the first sync, and it puts data on devices that have no reason to see it.
A sync scope is the answer to that question. It draws a line around a set of shared data and a set of participants, and the sync engine only moves data that falls inside the line. Even cloud databases with an offline mode work this way in a simple form: Cloud Firestore’s offline persistence caches the data the app is actively using, not the whole database.
Two jobs in one word
The word scope covers two separate jobs, and keeping them apart makes designs clearer.
- Who shares. A group of participants that exchange a body of data, such as a team, a job site, or two paired devices. Some systems call this a space or a group.
- What gets copied. A filter that selects part of that body of data for a particular device, such as one project, one day’s jobs, or documents whose names start with a prefix. Systems call this a subscription, an interest, or a stream.
The first is a sharing boundary. The second is a convenience for storage and bandwidth inside that boundary.
How sync systems draw the line
Different sync systems express scope in their own terms, and their documentation shows the range.
Ditto uses subscription queries. A device declares the data it wants with a query, for example every document in a cars collection where the colour is blue. Ditto broadcasts the subscription to connected peers, and any peer holding matching data syncs it to the subscribing device, whether that peer is another phone or Ditto’s server.
PowerSync uses Sync Streams. Developers write SQL-like queries that define streams, and the client app subscribes to the streams it needs, so each client syncs only the relevant subset rather than the whole database. The service groups data into buckets, creating one bucket for each unique value of a stream’s filter, such as one per user ID.
Both are scoping tools. Neither, on its own, is what keeps a device out of data it should not have.
Scope is not access control
A subscription is a request. Anything the requester controls can be changed by the requester, so a filter chosen on the device cannot be the thing that protects the data.
PowerSync’s documentation says this directly about parameters a client sends: a client can pass any value, so they should not be used for access control. Its access rules instead rely on parameters taken from the user’s signed authentication token. Ditto keeps permissions separate from subscriptions, expressed per collection, and its documentation states they are enforced both by its server and by every device taking part in sync in the mesh, so a client that asks for a document it is not authorised for does not receive it.
Peer-to-peer sync makes this harder, because there may be no server in the path to say no. The Ink & Switch essay that defined local-first software points out that any user who has a copy of some data cannot be prevented from modifying it locally; other users can only choose whether to accept those changes. In practice, the boundary that holds is the one enforced by encryption and membership: a device that never received the keys for a group cannot read that group’s data, whatever it asks for. Who can read shared data in a local-first app covers this in more detail.
Offline Protocol follows the same split. A space is an existing MLS session or group, and setInterest narrows which documents in that space a device replicates. Its shared state guide states that interests reduce replicated data but are not a substitute for membership authorisation, and that separate spaces should be used for different access boundaries.
Choosing scopes for an app
A few rules follow from the split between sharing and filtering:
- Draw sharing boundaries along access boundaries. If two groups of people must never see each other’s data, they need separate spaces or groups, not one space with different filters.
- Use filters for size. Inside a boundary, narrow what each device copies so that a phone holds this week’s jobs, not every job ever recorded. How big a local-first document can get explains why this matters.
- Plan for leaving. Removing someone from a scope stops future replication. It does not reach into their device and delete what is already there, so deletion needs its own design.
- Keep scope names stable. Devices that have been offline for a while will come back expecting the same spaces and names. Renaming a scope looks like a new, empty one to them.
Scope decides what moves. The route it moves by, directly between nearby devices or through a server, is a separate choice, covered in peer-to-peer sync vs cloud sync.