Skip to contentMenu Download OpenVireo

When two devices change the same vault, a sync combines both sets of changes instead of keeping one and losing the other. It first merges each entry edited on both sides field by field, against the last version both had (the base copy): a field only one side changed takes that side’s value, and a field both changed takes the newer one and is reported in SyncResult.conflicts. Then it merges the structure (entries and groups added, moved or deleted), as specified below. Most apps only need this paragraph; the rest is the exact specification.

Structure merge: required behaviour

mergeInto(source, target, mode) brings one database’s changes into another. Sync runs it twice (local → remote, then remote → local) after the field-by-field step (combineEntries), so both copies end up equal. The OpenVireo Qt client syncs the same files, so the observable result must match what it produces; packages/sdk/test/fixtures/merge-fixtures/ holds results recorded from the Qt client.

This document is the specification packages/sdk/src/merge/tree.ts is written from. It describes outcomes only.

Terms

  • Time of an entry or group: its last modification time, compared in whole seconds (KDBX files store seconds, so a copy that was saved and reopened has lost its milliseconds). A missing time counts as 0.
  • Location time: the time an item last changed parent. Compared in milliseconds, missing = 0.
  • Counterpart: the item with the same UUID anywhere in the target tree. The matching group for a source item is the target group that corresponds to the source item’s parent (the target root for the source root).
  • Copy of an entry: an entry with the same UUID and all of the original’s data (fields and their protection, attachments, times, icon, colours, tags, URL override, auto-type, custom data items, quality-check flag, previous parent), its attachments added to the target’s binary pool and its custom icon added to the target’s icons when missing there. A full copy also has copies of the history items (which never have history of their own); a plain copy has none.
  • Appending an item to a group detaches it from its current parent and adds it as the group’s last child.
  • History limit: the target’s historyMaxItems. Trimming keeps only the newest items when the limit is set and not negative.

Change records

Every action returns records {type, uuid?, title?, group?, details}. For an entry: its UUID, its Title and its parent group’s name. For a group: its UUID and its own name in group. For database-wide actions: only type and details. The records are listed in this order: tree walk, then deletions, then metadata.

1. Tree walk

Visit the source tree from its root. For each source group, handle its entries in order first, then its child groups in order. Each child group is handled (step 1.2) before its own contents.

1.1 Entries

  • No counterpart: append a full copy to the matching group. Record added, details "".
  • Counterpart exists:
    1. If the source entry’s location time is later than the counterpart’s and the counterpart is not in the matching group, append the counterpart to the matching group (its location time is not changed). Record moved for the source entry, details Previous location: <name of the counterpart's old parent>.
    2. Then compare times:
      • Counterpart older: replace it with a full copy of the source entry. The copy’s history is merged (§2) with the counterpart as the other side, and the copy is appended to the group the counterpart was in. Always record modified for the counterpart (its title and group as they were), details Add local modifications to new entry.
      • Same time or counterpart newer: merge the source entry’s history into the counterpart (§2). Record modified, details Add new modifications to existing entry, only if that changed anything.

1.2 Groups

  • No counterpart: create a group with the source group’s own properties (name, notes, icon, times, flags, custom data; no children), append it to the matching group, and give it the source’s location time. Add its custom icon to the target’s icons when missing. Record added, details Number of entries in group: <number of entries directly in the source group>.
  • Counterpart exists:
    1. If the source’s location time is later and the counterpart is not in the matching group, append it there and copy the source’s location time. Record moved, details Previous location: <the counterpart group's own name>.
    2. If the counterpart’s time is earlier than the source’s (whole seconds), update it and record modified with details listing what changed, joined by ", ", in this order:
      • Group name when the names differ (take the source’s);
      • Notes when they differ (missing = empty);
      • icon: if the source has a non-empty custom icon whose UUID differs from the counterpart’s, take it (Icon (UUID)) and add the icon image to the target when missing; otherwise, if the standard icon numbers differ or the counterpart has a custom icon, take the source’s number and clear the custom icon (Icon (Number));
      • Expiry time when the expiry times differ in whole seconds (take the source’s);
      • always Modification time (take the source’s).
  • Then walk the source group’s contents into its counterpart (or the new group).

2. History merge

Merging the history of entry S (other side) into entry T (the one being kept):

  1. Index history items by their time in whole seconds; when two share a time, the later one wins. Start from copies of T’s history items.
  2. Add each of S’s history items whose time isn’t in the index yet. If S is newer than T, S’s items also replace T’s items with the same time.
  3. If T is older than S, add a plain copy of T itself when its time isn’t present. If T is newer than S, add a plain copy of S itself when its time isn’t present. If they have the same time, add neither.
  4. Sort by time, oldest first.
  5. Decide whether this counts as a change by comparing the old and new lists from the newest end. With N = the history limit (10 if unset), the positions checked are the last N - 1 items of each list (so a limit of 0, 1 or a negative number never counts as a change). It is a change as soon as a position is filled in only one list or holds items that differ (§3).
  6. Only if it is a change: T’s history becomes the new list, trimmed to the history limit.

3. Entry equality

Two entries are equal when all of these match: UUID; the same set of fields (attributes, including the five standard ones, which count as present and empty when missing) with equal text and equal protection flags; attachments by name and content; tags; expiry (whether it expires and when, in seconds); icon (custom icon UUID, or the standard number); foreground and background colours; modification, creation, last access and location times in whole seconds; usage count; URL override; auto-type settings; custom data keys and values; previous parent group; and their histories, item by item.

4. Deletions

Only in synchronize mode; keep-newer (the sync default) skips this step and keeps the target’s list of deleted objects as it is.

  1. Go through the target’s deletion records, then the source’s, skipping any without a UUID or a time. The first record for a UUID decides what it refers to: an entry in the target, a non-root group in the target, or nothing there (then that first record is kept as is). A later record for the same UUID with an earlier time becomes the record used for that entry or group.
  2. Each entry with a record: if it was modified after the deletion time (milliseconds), keep the entry and drop the record. Otherwise remove the entry, keep the record, and record deleted with details Explicit deletion.
  3. Groups with a record, deepest first (a group waits while any of its child groups are still waiting): keep it and drop the record if it was modified after the deletion time or still contains any entries or groups. Otherwise remove it, keep the record and record deleted (Explicit deletion).
  4. If the resulting list (UUIDs and times, in order) differs from the target’s current list, it replaces it, recorded as metadata with details Changed deleted objects.

5. Database metadata

  1. Each custom icon of the source missing in the target is added: metadata, Adding new icon <uuid>.
  2. Database custom data. Its “last modified” time is the _LAST_MODIFIED item’s value when that parses as a date, otherwise the latest lastModified of its items. The source’s custom data is applied when the target has no _LAST_MODIFIED item, or when both times exist and the target’s is earlier. Applying it:
    • removes target items the source doesn’t have (Removed custom data <key> [<value>]), except the protected keys: _CREATED_, FDO_SECRETS_EXPOSED_GROUP, KPXC_REMOTE_SYNC_SETTINGS, KPXC_SYNC_GENERATION and every key starting with KPXC_BROWSER_;
    • copies every source item whose value differs (Adding custom data <key> [<value>]), except the generated keys _LAST_MODIFIED, KPXC_RANDOM_SLUG and KPXC_SYNC_GENERATION.

The SDK docs cover OpenVireo SDK, develop (86e6cd2), last updated 26 October 2026. Found a mistake? Edit this page on GitHub.