Joplin synchronisation
The Joplin applications are offline first - it means that data is saved locally on the device. In order to have the same data on all the user's devices, we use a synchronisation process. In a nutshell, each device uploads its notes, notebooks, tags, etc. to the server, and also downloads any notes they do not have, or any recent changes. If a note is deleted, it is also deleted from the server, and eventually deleted from each device too.
Vocabulary
Clients
The sync clients are the Joplin applications - the desktop, mobile and terminal applications.
Sync targets
The sync target is the location where the data is going to be saved. It can be for example Joplin Server, a Nextcloud instance, or a WebDAV server.
Items
The "items" are the notes, notebooks, tags and resources that need to be synced.
General process
Whenever the user makes a change to an item, it is uploaded to the sync target within a few seconds. Uploading items as soon as possible helps limit conflicts. Because that way, any client that connects to the sync target is more likely to get the latest version of the item.
Additionally, every few minutes, the client is going to poll the server and download the latest changes, and apply them to the local note collection.
Code architecture
packages/lib/Synchronizer.ts
: This file is responsible for the main synchronisation process. It download changes, upload them, and apply any deletion. The class is relatively generic and receive aSyncTarget
object that handles sync target-specific operations. The synchroniser is also going encrypt and decrypt items if E2EE is enabled.packages/lib/SyncTarget*.ts
: These files are the entry points for the various sync targets. They expose some metadata such as name, description, what options they support, etc. Some may also implement a function to test whether the configuration is working (used from the configuration screen). Finally, the main role of this class is to initialise an instance of aFileApi
.packages/lib/file-api-driver-*.ts
: Those are the file APIs. They must implement generic file-like operations to create, update, delete or list files. This API is in turn used by the synchroniser to created, update or delete items.packages/lib/*Api.ts
: Thefile-api-driver
will call some low-level API to perform its operations. For examplefile-api-driver-local
will use thefs
package to read/write files,file-api-driver-amazon-s3
will use the AWS API to work with S3. In some cases however such a low-level API is not available - in that case, we usually create an*Api.ts
file, which is used by the file API driver to perform its operations. For example, there is aJoplinServerApi.ts
, which is used to connect to Joplin Server.- In general, each object in the database is represented by a
BaseModel
class. Then each object than can be synced is represented by aBaseItem
class that inherits fromBaseModel
. This class is where many sync-related utilities can be found such asitemsThatNeedSync()
or methods that encrypt items so that they can be uploaded when E2EE is enabled. - The state of each item is saved to the
sync_items
table. There is saved in particular thesync_time
property which tells when the item was last synced. It is then used to decide what needs to be synced or not. Additional sync-related properties includesync_disabled
, which is used in the rare case an item cannot be synced at all - for example if blocked by Dropbox for being "restricted content" (copyrighted), or is over the limit on Joplin Cloud. Each entry insync_items
is scoped to a sync target (sync_target
property), so theoretically it's possible to sync the same items to multiple sync targets.
Testing
By default, the test units synchronise with an in-memory sync target, which is fast and is usually enough to verify most behaviours. The test units however can be configured to sync with a specific sync target, such as the file system, Nextcloud, Joplin Server, etc. To do so, modify packages/lib/testing/test-utils.ts
and change setSyncTargetName()
to the relevant sync target. You may also need to add or modify the relevant files in ~/joplin-credentials/*
. See the initFileApi()
method in test-utils.ts
for more details.
Sync target properties
The properties specific to the sync target are saved in a file called info.json
- it is used to ensure all clients work with the same sync settings.
Several properties have an associated updatedTime
property. This is used to resolve conflicts when two clients perform the same action on the same property. In this case heuristics decide which value should be kept. See packages/lib/services/synchronizer/syncInfoUtils.ts
for all logic related to this info.json
file.
Each client keeps a local version of this sync target info, which is synchronised whenever it is modified.
The list of properties is:
interface SyncTargetInfo {
// The sync target version number.
version: number;
// Whether E2EE is enabled on the sync target or not
e2ee: {
value: boolean;
updatedTime: number;
}
// The active encryption key
activeMasterKeyId: {
value: string;
updatedTime: number;
}
// The known encryption/decryption keys
masterKeys: Key[];
// The public/private keys
ppk: {
value: {
id: string;
keySize: number;
privateKey: Key; // Encrypted using user password
publicKey: string; // Plaintext
createdTime: number;
}
}
// The minimum app version required to sync with this sync target
appMinVersion: string;
}