Skip to main content

PowerSync: Designed for Causal+ Consistency

PowerSync is designed to have causal+ consistency, while providing enough flexibility for applications to perform their own data validations and conflict handling. PowerSync’s consistency properties have been tested and verified.

How It Works: Checkpoints

A checkpoint is a single point-in-time on the server (similar to an LSN in Postgres) with a consistent state — only fully committed transactions are part of the state. The client only updates its local state when it has all the data matching a checkpoint, and then it updates the state to exactly match that of the checkpoint. There is no intermediate state while downloading large sets of changes such as large server-side transactions. Different tables and buckets are all included in the same consistent checkpoint, to ensure that the state is consistent over all data in the client.

Client-Side Mutations

Client-side mutations are applied on top of the last checkpoint received from the , as well as being persisted into an upload queue. While mutations are present in the upload queue, the client does not advance to a new checkpoint. This means the client never has to resolve conflicts locally. Only once all the client-side mutations have been acknowledged by the server, and the data for that new checkpoint is downloaded by the client, does the client advance to the next checkpoint. This ensures that the operations are always ordered correctly on the client.
There is one nuanced case here, which is buckets with Priority 0 if you are using Prioritized Syncing.

Types of Client-Side Mutations/Operations

The client automatically records mutations to the client-side database as PUT, PATCH or DELETE operations — corresponding to INSERT, UPDATE or DELETE statements in SQLite. These are grouped together in a batch per client-side transaction. Since the developer has full control over how mutations are applied to the source database, more advanced operations can be modeled on top of these three. See Custom Conflict Resolution for examples.

Validation and Conflict Handling

With PowerSync offering full flexibility in how mutations are , it is also the developer’s responsibility to implement this correctly to avoid consistency issues. Some scenarios to consider: While the client was offline, a row was modified on the client-side. By the time the client is online again, that row has been deleted on the source database. Some options for handling the mutation in your backend:
  • Discard the mutation.
  • Discard the entire transaction.
  • Re-create the .
  • Record the failed mutation elsewhere, potentially notifying the user and allowing the user to resolve the issue.
Some other examples include foreign-key or not-null constraints, maximum size of numeric fields, unique constraints, and access restrictions (such as row-level security policies). In an online-only application, the user typically sees the error as soon as it occurs, and can correct the issue as required. In an offline-capable application that syncs asynchronously with the server, these errors may occur much later than when the mutation was made, so more care is required to handle these cases. Special care must be taken so that issues such as those do not block the upload queue. The upload queue in the PowerSync Client SDK is a blocking FIFO queue, and the queue cannot advance if the backend does not . And as mentioned above, if the queue cannot be cleared, the client does not move on to the next checkpoint of synced data. There is no single correct choice on how to handle write failures such as mentioned above — the best action depends on the specific application and scenario. However, we do have some suggestions for general approaches:
  1. In general, consider relaxing constraints somewhat on the backend where they are not absolutely required. It may be better to accept data that is somewhat inconsistent (e.g. a client not applying all expected validations), rather than discarding the data completely.
  2. If it is critical to preserve all client mutations and preserve the order of mutations:
    1. Block the client’s upload queue on unexpected errors (don’t in your backend API).
    2. Implement error monitoring to be notified of issues, and resolve the issues as soon as possible.
  3. If it is critical to preserve all client mutations, but the exact order may not be critical:
    1. On a constraint error, persist the transaction in a separate queue on your backend, and acknowledge the change.
    2. The backend queue can then be inspected and retried asynchronously, without blocking the client-side upload queue.
  4. If it is acceptable to lose some mutations due to constraint errors:
    1. Discard the mutation, or the entire transaction if the changes must all be applied together.
    2. Implement error notifications to detect these issues.
See also:

Questions?

If you have any questions about consistency, please join our Discord to discuss.