Skip to content

Conversation

@fosple
Copy link

@fosple fosple commented Oct 26, 2025

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-27382

📔 Objective

This PR is the first step of adding Offline Editing capabilities to Bitwarden, which is one of the highest requested features by the community:
https://community.bitwarden.com/t/offline-editing-management-of-writeable-vault-items/107

This work implements the foundation for conflict detection, conflict-safe sync, and item history/restore.

High-level goal

Enable Bitwarden clients to safely sync edits made offline or concurrently on multiple devices without losing data, while keeping full end-to-end encryption and zero-knowledge guarantees.

This PR introduces:

  • A conflict-aware sync model based on Last-Write-Wins (LWW) semantics for the canonical version, plus encrypted history snapshots of every successful server-side edit for safety and auditability.
  • Client-driven conflict resolution that never exposes plaintext to the server.
  • APIs that let clients view and restore historical versions of an item.

This is the enabling infrastructure for Offline Editing. It is intentionally easy to implement for all clients (mobile, desktop, browser).

Clients only need to:

  • Detect sync conflicts (HTTP 409),
  • Store both versions locally,
  • Decide which version to keep or to merge them
  • Surface item history for restore.

It is also backwards compatible: Existing clients that do not support conflict resolution continue to function.

Behavioral summary

General behaviour

  1. Every cipher update now snapshots the previous state into encrypted history.
  2. If two devices modify the same item independently (classic offline edit case), a sync conflict is detected.
  3. The server responds with HTTP 409 (Conflict) and returns the server’s current cipher.
  4. The client then:
    • Preserves both its local edit and the remote/server version on the device.
    • Decides which version should become canonical. E.g. merge fields or just selects one.
    • Uploads that selected version as the new canonical item, using the latest RevisionDate.
  5. The server automatically stores the previous canonical state in history on each successful write. Client versions that never become canonical are not persisted automatically.

Proposed client behavior

Several client behaviors are supported with this PR. However, the following one is the one I think is the best from a UX and technical perspective.

Proposed behavior for clients:

  1. Client A creates cipher C and syncs it to server.
  2. Client B syncs with server and downloads cipher C
  3. Client A and B now have same version of cipher C (version 0) on local device
  4. Client A and Client B go offline
  5. Client A edits cipher C while being offline - it's now cipher C (version A) (version with client A edits).
  6. Client B edits cipher C while being offline - it's now cipher C (version B) (version with client B edits).
  7. Client A comes online and syncs cipher C (version A) to server.
  8. Server does not detect any sync conflict. Server stores cipher C (version 0) to history and makes cipher C (version A) canonical.
  9. Client B comes online and tries to sync cipher C (version B).
  10. The server responds to Client B with HTTP 409 (Conflict) and returns the server’s current cipher C (version A).
  11. Client B decides own version of cipher C (version B) should become canonical (Client always decides it's own version becomes canonical: last-write-wins!)
  12. Client B loads the server copy from the 409 response, apply the local edits onto it to form the new payload [replace cipher C (version A) with cipher C (version B)], set its LastKnownRevisionDate to cipher (version A)’s revision, then sync.
  13. The server automatically stores the previous canonical state cipher C (version A) in history and set's clients version cipher C (version B) as canonical.

This approach:

  • Does not need any user interaction - so sync is always automatic
  • Ensures all versions are always saved to history. So user can always go back to any version of cipher at any point in time. This ensures no data loss.
  • Avoids server-side merge logic (keeps server stateless with respect to plaintext).
  • Keeps sync simple and robust.
  • Works with fully encrypted ciphers, because all merge/selection logic runs locally after decryption on the client.
  • Guarantees that server-accepted revisions are never silently dropped — even in multi-device race conditions.

📸 Screenshots

No UI changes in this PR. The new history/restore endpoints will allow future clients to present item history and conflict views, but no user-facing UI is added here.

Changes

New internal logic

  • Added encrypted history tracking: created the CipherHistory table/sql, entity, repositories (Dapper + EF), service registrations, and a DbUp migration (with CipherHistory_Create proc, index, FK) so every cipher update persists a snapshot.
  • Updated CipherService to look up the current record, throw a new SyncConflictException when revisions diverge, log history entries before mutations (including attachment paths and admin deletes), and reuse that helper across flows.
  • Implemented conflict-aware API behavior: CiphersController now wraps write endpoints to surface HTTP 409 responses with the server cipher for user and admin scenarios, plus shared helper methods.
  • Registered the new repositories, exposed the history DbSet on DatabaseContext, and introduced the SyncConflictException type.

New API endpoints

  • Added GET /ciphers/{id}/history
  • Added POST /ciphers/{id}/history/{historyId}/restore
  • Created CipherHistoryResponseModel to serialize history snapshots.
  • Implemented ICipherService.RestoreFromHistoryAsync to snapshot the current cipher, apply historical data, log, and push updates; updated interface accordingly (src/Core/Vault/Services/*).

Data access

  • Extended ICipherHistoryRepository with GetManyByCipherIdAsync and custom GetByIdAsync; added SQL-based implementations for both Dapper and EF providers to fetch histories ordered by timestamp (src/Core/Vault/Repositories/ICipherHistoryRepository.cs, src/Infrastructure.*).

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@fosple fosple requested review from a team as code owners October 26, 2025 16:43
@CLAassistant
Copy link

CLAassistant commented Oct 26, 2025

CLA assistant check
All committers have signed the CLA.

@bitwarden-bot
Copy link

Thank you for your contribution! We've added this to our internal tracking system for review.
ID: PM-27382
Link: https://bitwarden.atlassian.net/browse/PM-27382

Details on our contribution process can be found here: https://contributing.bitwarden.com/contributing/pull-requests/community-pr-process.

@bitwarden-bot bitwarden-bot changed the title [Offline Editing] Add core infrastructure for offline-safe edits and version history [PM-27382] [Offline Editing] Add core infrastructure for offline-safe edits and version history Oct 26, 2025
@fosple
Copy link
Author

fosple commented Nov 7, 2025

@jaasen-livefront Updated description and added Proposed client behavior to let you understand better how sync will work with clients.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants