Self-Hosted Sync
This content is for the 1.0.0 version. Switch to the latest version for up-to-date documentation.
Sync keeps servers, keys, snippets, dashboards, vault items and other configuration in step across your devices through a PocketCmd Server you run yourself. Records are encrypted on the device with AES-256-GCM before upload. The server stores the ciphertext plus the metadata it needs to route and authorize it. Sync requires PocketCmd Pro.
PocketCmd does not operate a sync service. The services PocketCmd does run are the app stores’ billing, the license server that activates and checks Pro license keys and the push relay that carries alert notifications. None of them receives your configuration.
Which server a record syncs to
Section titled “Which server a record syncs to”Every synced item belongs to a vault:
- Personal vault. Syncs to every paired server you turn it on for, with Synchronize personal vault on the server’s entry under Teams. Only your own devices can read it. Turning it on sets up your personal vault on that server, or unlocks it if you already have one.
- Team vaults. Live on the team’s server and sync to the members who have access. Access levels and sharing are covered in Secrets Vault.
What syncs
Section titled “What syncs”The server accepts these record types (internal/datasync/store.go):
| Record type | Contents |
|---|---|
task |
Saved servers: connection settings, with passwords and key passphrases in the separately encrypted credentials field. |
task_group, task_tag |
Server groups and tags. |
ssh_key |
SSH keys. The private key travels in the encrypted credentials field. |
sftp_bookmark |
SFTP bookmarks. |
workspace |
Workspaces: server, root folder, name and settings. Open tabs and unsaved drafts stay on the device. |
snippet, snippet_folder |
Snippets and their folders. |
command_template |
Command templates. |
layout_config |
Split-screen layout presets. |
port_forward_rule |
Port forwarding rules. |
dashboard, alert_config |
Dashboards and their alert settings. |
settings |
App theme, theme mode (system, light or dark) and terminal font size. |
secret_item |
Vault items: logins, notes, API keys and the other item types. |
vault_wrap, vault_meta |
Vault keys wrapped for each recipient device and vault metadata. |
device_vault_key, enroll_request, device_revocation |
The per-device vault key directory: public keys, pending device approvals and permanent revocations. Public material only. |
files_enrollment |
Which devices enrolled a key for System File Manager access on which server, so your other devices can clean up after a removed device. Contains no secrets. |
monitoring_config |
The team’s approved metric commands and their alert rules. Plaintext, see below. |
Quick links, the SFTP transfer queue, local metric history and other device preferences are not synced.
Encryption
Section titled “Encryption”- Each device has its own vault key pair, generated in the device’s secure element (Secure Enclave, Android StrongBox or TEE, TPM 2.0) where one is available. Only the public half is published, as a
device_vault_keyrecord. - Each vault has a random 256-bit key. It is wrapped separately for every device that may read the vault, so adding or removing a device changes the wraps, not the records.
- Your app password is stretched with Argon2id (64 MiB, 3 passes, 1 lane) and mixed into the key that opens your own wraps. Opening the vault takes the device and the password; the password never leaves your devices.
- Each record is encrypted with AES-256-GCM under its vault key with a fresh 96-bit nonce, then pushed to the server.
- Other devices pull the record, unwrap the vault key with their own device key and decrypt locally.
Removing a device or a team member rotates the keys of the vaults it could read. Records are re-encrypted under the new key generation on their next push.
The server never holds a key derived from your password. A copy of the server’s database gives an attacker nothing to guess a password against offline.
What the server can see
Section titled “What the server can see”- Sync metadata: record type, record ID, size, update time, the ID of the device that wrote it, the owning user and vault membership. The server needs these fields to route records, check permissions and resolve conflicts.
monitoring_config: stored in plaintext because the server executes the metric commands and evaluates the alert rules in it. It is shared by the whole team. Never put secrets in metric commands or labels.- Team directory: display names, device names, roles and permissions.
Record contents (hostnames, usernames, key material, snippet bodies, vault items) are opaque to the server.
| Layer | Primitive |
|---|---|
| Password stretching | Argon2id, 64 MiB, 3 passes, 1 lane |
| Device keys | Non-exportable where a secure element exists: Secure Enclave, StrongBox or TEE, TPM 2.0 |
| Record encryption | AES-256-GCM, 96-bit nonces |
| Transport | TLS 1.3, mutual TLS, session token bound to the client certificate |
When sync runs
Section titled “When sync runs”There is no background timer. A sync pass runs on:
- App start, after unlock.
- An edit, 5 seconds after the last change.
- Returning to the app, when the last successful pass is more than 30 minutes old.
- Opening the vault and pull-to-refresh in the vault.
- Pairing a server and turning on personal vault sync for it.
A failed pass is retried once after 30 seconds. After that, the next trigger in the list above starts a new attempt.
When Pro ends, sync stops and no timers stay armed. Local edits keep being recorded. When Pro is active again, one full pass pushes everything that changed in the meantime and pulls what other devices pushed.
Conflicts and deletions
Section titled “Conflicts and deletions”The newest edit wins. Each record carries the time of its last local edit, and the server rejects a push whose timestamp is not newer than the stored one. The newer version reaches the device on its next pull. A timestamp more than 5 minutes ahead of the server’s clock is refused, so a device with a wrong clock cannot win every conflict. The server records which device and user wrote each record itself and ignores what the device claims.
The app pushes and pulls in batches. When the server refuses a record for a reason that resending won’t fix, such as missing vault access or a permission the member doesn’t hold, the app stops pushing that version and tries again only after the record is edited.
A deletion is stored as a tombstone: the record keeps its ID and type, its contents are cleared and it is marked deleted. Tombstones are kept for 365 days so that a device that was offline for a long time cannot bring a deleted record back, then removed by a daily cleanup. A device offline for more than a year can still restore a record deleted in the meantime.
A record cannot move between vaults while it exists. Moving an item deletes it from the old vault and creates it in the new one.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Push request size | 10 MB |
| Record size | 1 MB for the data and 1 MB for the credentials field |
| Records per push | 10,000 |
| IDs per pull | 10,000 |
All endpoints require mTLS, a session token and the X-PocketCmd-Api-Version header.
| Endpoint | Method | Purpose |
|---|---|---|
/api/sync/manifest |
GET | Record IDs with type, update time and deleted flag, limited to the records the caller may read. |
/api/sync/pull |
POST | Fetch records by ID. |
/api/sync/push |
POST | Write new and changed records. The response lists accepted records and the reason for every rejected one. |
Each member sees their own records, the team’s monitoring_config, the public device directory and the team-vault records they have access to.
Storage
Section titled “Storage”Sync records live in the sync_records table of ~/.pocketcmd/state.db. monitoring_config is the exception: it is split into its own tables so the scheduler can read it.
CREATE TABLE sync_records ( id TEXT NOT NULL, type TEXT NOT NULL, schema_version INTEGER DEFAULT 1, updated_at INTEGER NOT NULL, device_id TEXT DEFAULT '', deleted INTEGER DEFAULT 0, data TEXT DEFAULT '{}', -- ciphertext credentials TEXT DEFAULT '', -- ciphertext user_id TEXT NOT NULL DEFAULT '', vault_id TEXT NOT NULL DEFAULT '', PRIMARY KEY (type, id));Lost devices and forgotten passwords
Section titled “Lost devices and forgotten passwords”Each device holds its own vault key, so pairing again does not recover anything on its own. To add a device, approve it from a device that is already enrolled: the enrolled device shows a six-digit code that you type on the new one. If you forget the app password, unlock on another enrolled device and enroll this one again. If no enrolled device is left, the vault cannot be recovered. See App Password & Vault.