The challenge of maintaining a consistent set of files across multiple, intermittently connected devices is a classic problem in distributed computing. While seemingly straightforward, the task is fraught with subtle complexities that can lead to data loss, corruption, and frustrating user experiences. The development of a custom synchronization plugin for Obsidian, leveraging Google Drive as a central repository, presents an opportunity to engineer a solution that is not only functional but also robust, resilient, and deeply integrated with the Obsidian API. This report moves beyond simplistic approaches to file synchronization and details a comprehensive, production-grade system design. It provides a formal architecture, a deterministic synchronization algorithm, and robust strategies for handling the most challenging aspects of distributed state management, including conflict resolution, deletion propagation, and system stability.
The core of the proposed solution rests on a few fundamental principles. First, it acknowledges the nature of Google Drive as a passive data store, which places the full burden of synchronization intelligence upon the client applications—in this case, the Obsidian plugin itself. Second, it establishes a rigorous, stateful approach to change detection, moving beyond unreliable file modification times to a system based on server-authoritative revision histories and cryptographic hashes. This statefulness is the key to unambiguously identifying and resolving conflicts without data loss. Finally, the design addresses critical failure modes and edge cases, such as the "zombie file" problem and infinite sync loops, with specific, algorithm-driven solutions. This document serves as a complete blueprint, guiding the developer from high-level architectural patterns to the specific Google Drive and Obsidian API calls required for implementation, ensuring the final tool is both powerful and trustworthy.
The selection of an appropriate architectural pattern is the most critical initial decision, as it dictates the responsibilities of each component and the flow of information within the system. Given the use of Google Drive, a generic cloud storage service, the optimal design is a "Passive Hub, Intelligent Spokes" model, a specialized variant of the traditional client-server architecture.1
In this model, the "hub" is Google Drive. It serves as the central, canonical repository for all synchronized files—the single source of truth.4 However, it is considered "passive" because it does not actively participate in the synchronization logic. The Google Drive API provides primitives for storing and retrieving files and their metadata, but it does not offer services for delta calculation, conflict resolution, or orchestrated sync sessions.6
The "spokes" are the instances of the custom sync plugin running within Obsidian on each of the user's devices (desktop, laptop, mobile). These clients are "intelligent" because they contain the entirety of the synchronization logic. Each client is independently responsible for reconciling its local state with the state of the hub, and by extension, with all other clients. This stands in contrast to peer-to-peer models, where devices may communicate directly, or enterprise three-way sync systems that employ an active central orchestrator to manage data flows and resolve conflicts.7 The constraint of a passive hub elevates the importance of the client-side logic and its ability to maintain a historical context of the synchronized state. Without this context, every synchronization attempt would be a blind, stateless comparison, making robust conflict detection and the prevention of stale overwrites impossible.
To enable intelligent, stateful synchronization, the plugin must maintain a persistent, local database that acts as its memory of the last known synchronized state. This "Local State Index" formalizes the user's existing concept of a local index and is the most critical data structure in the entire system. Within the Obsidian plugin environment, this is best implemented as a JSON object stored in the plugin's dedicated data file (data.json).27 The Obsidian API provides convenient methods,
this.loadData() and this.saveData(), to manage this file, abstracting away the filesystem details.28
The integrity of this index is paramount. It transforms the synchronization process from a simple two-way comparison (local vs. remote) into a more sophisticated three-way analysis: the last known common state, the current local state, and the current remote state. This three-point comparison is the foundation upon which reliable conflict detection is built.
Each record in the Local State Index corresponds to a file being tracked by the sync plugin and must contain the following fields:
- filePath: The relative path of the note within the local Obsidian vault (e.g., notes/ProjectX/Meeting_Notes.md). This is used to locate the file on the local filesystem via the Vault API.
- fileId: The unique and immutable ID assigned by the Google Drive API when the file was first created.9 This serves as the unwavering link between the local file and its remote counterpart, even if the file is renamed or moved.
- lastSyncRevisionId: The headRevisionId of the Google Drive file as it existed at the conclusion of the last successful sync operation involving this file.10 This field is the client's record of the specific server version it is in sync with.
- lastSyncHash: The cryptographic hash (e.g., SHA-256) of the local file's content, calculated at the same time lastSyncRevisionId was recorded. This provides a fast and reliable way to detect if the local file has been modified since the last sync, without needing to re-read the entire file content on every check.
- lastModifiedTime: The local file's modification timestamp provided by the operating system. While notoriously unreliable for comparing changes across different devices due to clock drift and timezone issues 12, it serves as an efficient, low-cost heuristic. A change in this timestamp can trigger the more expensive hash calculation to definitively confirm a local modification.
At the beginning of each full synchronization cycle, the plugin must construct a complete, up-to-date picture of the remote repository. This is not a persistent entity but rather a transient, in-memory data structure referred to as the "Server State Snapshot."
The plugin generates this snapshot by querying the Google Drive API to list all files and their relevant metadata within the target synchronization folder. All network requests must be made using Obsidian's requestUrl() function to ensure they work across desktop and mobile platforms by bypassing CORS restrictions.30 While a naive implementation might perform a recursive listing of the folder contents, a far more efficient method is to leverage the
changes.list API endpoint, which provides a chronological log of all modifications, creations, and deletions (detailed further in Section 5).10
The snapshot is effectively a map or dictionary where the key is the Google Drive fileId. Each entry in this map contains the essential metadata required for the reconciliation algorithm:
- remoteRevisionId: The current headRevisionId of the file on Google Drive. This is the server's authoritative version identifier.10
- remotePath: The current full path of the file within the Google Drive folder. This is used to detect remote renames or moves.
- remoteMetadata: A placeholder for any other relevant metadata, such as custom appProperties, which will be used for implementing advanced features like infinite loop prevention and deletion tracking.
With the persistent Local State Index and the transient Server State Snapshot defined, the plugin has all the information necessary to execute the core synchronization protocol.
The heart of the synchronization plugin is a deterministic, multi-phase algorithm that systematically compares states, transfers data, and commits the new state. This protocol ensures that every file is correctly categorized and processed, leading to a consistent state across all devices.
In this initial phase, the plugin performs a comprehensive comparison of the Local State Index, the actual state of the local filesystem (accessed via the Obsidian Vault API), and the newly generated Server State Snapshot. The objective is to sort every file into a specific action category, forming a clear execution plan for the subsequent phases. The power of this reconciliation process stems from its use of three data points for every decision: the last known synchronized state (from the index), the current local state (from the filesystem), and the current remote state (from the API). Simpler sync tools often omit the first data point, which prevents them from reliably distinguishing between a single, safe change and two independent, conflicting changes.
The following pseudocode outlines the decision-making logic, noting the relevant Obsidian API calls.
// Initialize action sets
filesToUpload =
filesToDownload =
filesInConflict =
filesToRenameLocally =
filesToRenameRemotely =
deletionsToProcess =
// Load the Local State Index from data.json
localStateIndex = await this.loadData()
// 1. Process files known from the last sync (present in Local State Index)
for localEntry in localStateIndex:
// Check for remote deletion
if not ServerStateSnapshot.contains(localEntry.fileId):
deletionsToProcess.add(localEntry)
continue
serverEntry \= ServerStateSnapshot.get(localEntry.fileId)
// Use Obsidian API to check local file existence
localFile \= this.app.vault.getAbstractFileByPath(localEntry.filePath)
if not localFile:
// Local file was deleted. This is handled separately (see Section 4.2).
continue
// Detect local and remote changes
// Use adapter.readBinary for hashing non-text files
fileData \= await this.app.vault.adapter.readBinary(localEntry.filePath)
currentLocalHash \= calculate\_hash(fileData)
localHasChanged \= (currentLocalHash\!= localEntry.lastSyncHash)
remoteHasChanged \= (serverEntry.remoteRevisionId\!= localEntry.lastSyncRevisionId)
// Decision Tree for file state
if localHasChanged and not remoteHasChanged:
filesToUpload.add(localEntry)
elif not localHasChanged and remoteHasChanged:
filesToDownload.add(localEntry)
elif localHasChanged and remoteHasChanged:
filesInConflict.add(localEntry)
elif not localHasChanged and not remoteHasChanged:
// Content is identical, check for metadata changes like path
if localEntry.filePath\!= serverEntry.remotePath:
filesToRenameLocally.add(localEntry, newPath=serverEntry.remotePath)
// else: No changes, do nothing.
// 2. Process new remote files (in Snapshot but not in Index)
for serverEntry in ServerStateSnapshot:
if not localStateIndex.contains(serverEntry.fileId):
filesToDownload.add(serverEntry)
// 3. Process new local files (on filesystem but not in Index)
// Use Obsidian API to list all files in the vault
allVaultFiles = this.app.vault.getFiles()
for localFile in allVaultFiles:
if not localStateIndex.containsPath(localFile.path):
filesToUpload.add(new_entry_for(localFile.path))
Once the reconciliation phase has produced a complete plan, the plugin executes the necessary data transfer and metadata update operations using the Obsidian Vault API for local changes and requestUrl for remote ones.30
For Obsidian notes, which are typically small text files, a full upload or download is simpler to implement and sufficiently performant. However, should the user store large attachments, implementing a delta-transfer mechanism based on rsync principles would be a valuable future optimization.14
The order of operations is critical to avoid race conditions. The plugin should execute actions in the following sequence:
- Process Renames: Apply renames using this.app.vault.rename(file, newPath).31
- Process Deletions: Handle deletions using this.app.vault.trash(file, true) for system trash or this.app.vault.delete(file) for permanent removal.32
- Execute Downloads: Transfer files from Google Drive and write them locally using this.app.vault.createBinary() or this.app.vault.modifyBinary().31
- Execute Uploads: Read local files with this.app.vault.adapter.readBinary() and upload to Google Drive.33
- Handle Conflicts: Execute the conflict resolution strategy (see Section 4.1).
This final phase is crucial for robustness. After each individual file operation in Phase 2 completes successfully, the Local State Index must be updated immediately and atomically for that file using this.saveData().27 This prevents the system from having to re-process completed transfers if the sync is interrupted.
- After a successful upload: The plugin receives a response from the Google Drive API containing the new headRevisionId. It must immediately update the corresponding entry in the Local State Index and commit it to disk with this.saveData().
- After a successful download: The plugin updates the local index entry with the remoteRevisionId of the downloaded version and the calculated hash of the new local file content, then calls this.saveData().
- After a successful local rename: The plugin updates the filePath field in the index and calls this.saveData().
By committing state changes on a per-file basis, the synchronization process becomes idempotent and resilient.
Determining when to initiate a synchronization cycle is a critical design decision that directly impacts user experience, network usage, and device battery life.15 The optimal solution for an Obsidian plugin is a hybrid model that combines multiple trigger types.
To provide immediate feedback, the plugin should react to local file changes in real-time. This is achieved using the Obsidian Vault's built-in event system, which abstracts away the complexities of native filesystem watchers.34 The plugin can subscribe to these events:
- this.app.vault.on('create', (file) => {... })
- this.app.vault.on('modify', (file) => {... })
- this.app.vault.on('delete', (file) => {... })
- this.app.vault.on('rename', (file, oldPath) => {... }) 31
All event handlers should be registered using this.registerEvent() to ensure they are properly cleaned up when the plugin is disabled or Obsidian is closed.34
A significant challenge is the phenomenon of "event storms," where a single save action generates multiple events.16 To manage this, the plugin must implement
debouncing and coalescing logic. When a file change event is received, a short timer (e.g., 2-5 seconds) is started or reset. When the timer elapses, a lightweight, targeted sync is triggered for the queued files.
To reliably detect changes made on other devices, the plugin must periodically poll the server. This is the backbone of the synchronization strategy. This can be implemented using window.setInterval(), ensuring it is registered with this.registerInterval() for proper cleanup on unload.34 This polling should use the efficient
changes.list Google Drive API endpoint and trigger the full three-phase synchronization algorithm.10 A polling interval of 5 to 10 minutes is a reasonable starting point.15
Providing the user with direct control is essential for building trust. A "Sync Now" command can be added to the Obsidian Command Palette using this.addCommand() or as a button in the left ribbon using this.addRibbonIcon().28 This manual trigger gives the user the agency to force a full synchronization cycle on demand.17
The most effective strategy is a hybrid approach that combines the strengths of all three triggers:
- For Local Changes: The debounced Vault event listeners provide a low-latency, real-time user experience.
- For Remote Changes: Periodic polling provides a reliable, low-overhead mechanism for discovering remote changes.
- For Robustness and Control: The periodic poll also acts as a self-healing mechanism, catching any local changes the event listeners might have missed. The manual trigger provides an essential override for the user.
| Trigger Type | Data Latency | Network Usage | CPU/Battery Impact | Implementation Complexity | Key Advantage | Key Pitfall |
|---|---|---|---|---|---|---|
| Event-Driven (Vault Events) | Very Low (Seconds) | Low (per event) | Low (event-driven) | Medium | Instantaneous feedback for local changes | Requires careful debouncing logic |
| Time-Driven (Polling) | Medium (Minutes) | Very Low (with changes.list) | Low (periodic spikes) | Medium | Highly reliable for detecting all remote changes | Inherent delay in discovering remote changes 15 |
| Manual (Command/Icon) | High (User-dependent) | Variable | Variable | Low | Provides user control and confidence | Not automatic; relies on user action 17 |
| Recommended Hybrid Model | Low (Blended) | Low (Optimized) | Low (Balanced) | High | Combines real-time feel with guaranteed reliability | Most complex to implement and test correctly |
A truly robust synchronization system is defined by how it handles failure modes and edge cases. This section addresses the most critical challenges: resolving conflicting edits, propagating deletions without error, and preventing the system from entering unstable states like infinite loops.
The most severe failure mode for a sync tool is silent data loss, which often occurs when conflicting edits are resolved improperly.
A conflict occurs if, and only if, a file has been modified independently on both the local device AND on the server since the last commonly agreed-upon version. This state is unambiguously detected during Phase 1 of the core algorithm when the following conditions are both true for a given file:
- The hash of the current local file does not match the lastSyncHash stored in the Local State Index.
- The headRevisionId of the remote file does not match the lastSyncRevisionId stored in the Local State Index.
- Last Write Wins (LWW): This approach, where one version overwrites the other, is highly discouraged as it guarantees data loss and is not a true conflict resolution strategy.18
- Conflicted Copy (Recommended): This strategy prioritizes data preservation above all else, as used by services like Dropbox . When a conflict is detected, the system preserves both versions, allowing the user to merge them manually.
The algorithm for implementing the "Conflicted Copy" strategy within an Obsidian plugin is as follows:
- Upon detecting a conflict for a file (e.g., note.md), the plugin does not overwrite any existing file.
- The plugin first downloads the server's version. Instead of overwriting, it saves it locally using this.app.vault.createBinary() under a new name, such as note (conflicted copy from Desktop on 2023-10-27).md.31
- Next, the plugin proceeds with its original plan to upload its local version of note.md.
- Finally, the plugin updates its Local State Index for note.md and creates a new entry for the conflicted copy file, treating it as a new local file to be uploaded on the next sync cycle.
This process ensures both sets of edits are preserved. The user can then use Obsidian's built-in file comparison or a community plugin with a diff view to merge the changes manually.36
| Strategy | Data Safety | User Experience | Automation Level | Implementation Complexity |
|---|---|---|---|---|
| Last Write Wins (LWW) | Low (Guaranteed Data Loss) | Poor (Changes disappear silently) | High (Fully automated) | Low |
| Conflicted Copy | High (Zero Data Loss) | Good (User is notified and empowered) | Medium (Requires manual merge) | Medium |
Handling deletions in a distributed system is deceptively complex and can lead to a "zombie file" problem where deleted files are resurrected by offline clients.21 The solution is to treat deletion as a state change using a "tombstone" or "soft delete" mechanism.24
Since Google Drive lacks a native tombstone feature, it must be implemented at the application layer using file metadata.
- Central Metadata File: The plugin designates a single, hidden file in the plugin's configuration directory (e.g., .obsidian/plugins/your-plugin-name/sync-tombstones.json). This file can be accessed using this.app.vault.adapter.read() and write().33
- Creating a Tombstone: When a user deletes a file locally (detected via vault.on('delete')), the plugin does not immediately delete the file on Google Drive. Instead, it adds a tombstone entry (the file's fileId and a timestamp) to the sync-tombstones.json file and uploads this file to a shared location on Google Drive.
- Processing Tombstones: During Phase 1, every plugin instance downloads and reads the central tombstone file. If it finds a tombstone for a file that still exists locally, it knows to delete its local copy using this.app.vault.trash().
- Garbage Collection: The actual file on Google Drive is only permanently removed after a "grace period" (e.g., 30 days).22 Any client, upon seeing a tombstone older than the grace period, can safely issue the final
files.delete API call and remove the tombstone entry.
An infinite sync loop is a catastrophic failure mode where clients endlessly pass the same change back and forth, often because a client misinterprets its own update as a new external change.26 The solution is to enable clients to recognize their own changes.
- Generate a Unique ID: On first run, the plugin generates a universally unique identifier (UUID) and stores it persistently in its data.json settings file using this.saveData().27 This is its
syncAgentId. - Tagging Uploads: Whenever the plugin uploads a file, it includes its syncAgentId in the request's appProperties field.
- Verifying Downloads: When the plugin detects a remote change, it first fetches the file's appProperties.
- Ignoring Echoes: If the lastModifiedByAgent property matches the plugin's own syncAgentId, it recognizes the change as an "echo" of its own modification and ignores it, breaking the loop.29
Translating the preceding algorithms into practice requires a precise understanding of the Google Drive API v3. All API calls from the Obsidian plugin must use the requestUrl() function to ensure cross-platform compatibility.30
Periodic polling is made highly efficient by avoiding a full file listing. The changes.list endpoint provides a "change log" that is the cornerstone of this efficiency.10
- Initial Setup (First-Time Sync): The plugin calls changes.getStartPageToken to get a startPageToken representing the current state of the Drive folder. This token must be stored persistently in the plugin's data.json file.
- Subsequent Sync Cycles: The plugin calls changes.list, passing the stored pageToken. The API returns a list of all Change resources that have occurred since that token was generated.10
- Processing the Change List: The plugin iterates through the changes to update its Server State Snapshot efficiently.
- Updating the Page Token: The response contains a newStartPageToken. The plugin must persist this new token in its data.json file, overwriting the old one, for the next sync cycle.
Modification timestamps are unreliable.13 The definitive, server-authoritative identifier for a specific version of a file's content is the
headRevisionId field.10 When a file is uploaded, the API response includes the new
headRevisionId. This is the value that must be captured and stored in the Local State Index as lastSyncRevisionId. Comparing the stored lastSyncRevisionId with the current headRevisionId from the server is the most robust method for detecting remote changes.
The Google Drive API allows applications to store custom, hidden key-value metadata on a file using the appProperties field.6 This is the perfect mechanism for storing the internal state required by the advanced algorithms in Section 4.
- Usage for Infinite Loop Prevention: When updating a file, the plugin includes the appProperties field in the files.update request body to tag the update with its syncAgentId.
- Example Request Body (partial): { "appProperties": { "lastModifiedByAgent": "uuid-for-client-A" } }
- Usage for Tombstones: When deleting a file, the plugin uses files.update on a central metadata file in Drive, adding a new key to its appProperties.
- Example Request Body (partial): { "appProperties": { "tombstone_1aBcDeF...": "1698432100",... } }
- Reading Metadata: To read these properties, the plugin issues a files.get request, using the fields query parameter to specify that appProperties should be included in the response, avoiding unnecessary data downloads.
By correctly leveraging these specific API features, the abstract synchronization protocol can be implemented in a way that is both correct and highly efficient within the Obsidian plugin environment.
The design and algorithm presented in this report constitute a comprehensive and resilient framework for a custom file synchronization plugin for Obsidian. By adopting the "Passive Hub, Intelligent Spokes" architecture, the system correctly places the burden of logic on the plugin, a necessity when using a generic cloud storage provider like Google Drive. The foundation of this intelligence is the Local State Index, managed via Obsidian's loadData/saveData API, which enables a stateful, three-point comparison that provides a definitive method for change and conflict detection.
The core of the system—a three-phase protocol of reconciliation, transfer, and committal—is tightly integrated with Obsidian's Vault and Adapter APIs for all local file operations. This is complemented by a hybrid triggering model that combines the real-time responsiveness of Obsidian's built-in vault.on() events with the guaranteed reliability of periodic polling, providing an optimal user experience without compromising robustness.
Crucially, this design directly confronts and solves the most difficult challenges in file synchronization. It rejects the data-loss-prone "Last Write Wins" strategy in favor of the professional "Conflicted Copy" approach. It implements a robust "Tombstone" mechanism to propagate deletions correctly. Finally, it breaks potential infinite sync loops by using a sync agent identity marker. By translating these advanced concepts into a practical implementation guide using specific features of both the Obsidian and Google Drive APIs, this report provides a complete blueprint for building a sync plugin that is not merely functional, but verifiably correct and trustworthy.
- Understanding Client-Server Architecture Basics - SynchroNet, accessed October 3, 2025, https://synchronet.net/client-server-architecture/
- Client–server model - Wikipedia, accessed October 3, 2025, https://en.wikipedia.org/wiki/Client%E2%80%93server_model
- Client-Server Architecture Explained with Examples, Diagrams, and Real-World Applications | by Harsh Gupta | Nerd For Tech | Medium, accessed October 3, 2025, https://medium.com/nerd-for-tech/client-server-architecture-explained-with-examples-diagrams-and-real-world-applications-407e9e04e2d1
- Data synchronization - Wikipedia, accessed October 3, 2025, https://en.wikipedia.org/wiki/Data_synchronization
- Client-server synchronization pattern / algorithm? - Stack Overflow, accessed October 3, 2025, https://stackoverflow.com/questions/413086/client-server-synchronization-pattern-algorithm
- Google Drive API overview, accessed October 3, 2025, https://developers.google.com/workspace/drive/api/guides/about-sdk
- 3-Way Sync vs 2-Way: What Really Matters in AI Projects - Boost.space, accessed October 3, 2025, https://boost.space/blog/3-way-sync-vs-2-way-what-really-matters-in-ai-projects/
- Is 3-Way Sync possible? : r/Syncthing - Reddit, accessed October 3, 2025, https://www.reddit.com/r/Syncthing/comments/i8ppor/is_3way_sync_possible/
- How to overwrite existing file content using Google Drive API while preserving file ID?, accessed October 3, 2025, https://community.latenode.com/t/how-to-overwrite-existing-file-content-using-google-drive-api-while-preserving-file-id/32169
- Changes and revisions overview | Google Drive | Google for ..., accessed October 3, 2025, https://developers.google.com/workspace/drive/api/guides/change-overview
- Manage file revisions | Google Drive, accessed October 3, 2025, https://developers.google.com/workspace/drive/api/guides/manage-revisions
- What algorithms are used for file sync (like Dropbox)? : r/AskComputerScience - Reddit, accessed October 3, 2025, https://www.reddit.com/r/AskComputerScience/comments/s6dvwl/what_algorithms_are_used_for_file_sync_like/
- Timestamp-based conflict resolution without reliable time synchronization - Stack Overflow, accessed October 3, 2025, https://stackoverflow.com/questions/18505299/timestamp-based-conflict-resolution-without-reliable-time-synchronization
- Rsync Algorithm - System Design - GeeksforGeeks, accessed October 3, 2025, https://www.geeksforgeeks.org/system-design/rsync-algorithm-system-design/
- Comparing Real-Time vs. Batch Synchronization for CRM Data ..., accessed October 3, 2025, https://www.stacksync.com/blog/comparing-real-time-vs-batch-synchronization-for-crm-data-when-each-makes-sense
- Monitoring Folders for File Changes - powershell.one, accessed October 3, 2025, https://powershell.one/tricks/filesystem/filesystemwatcher
- ArgoCD Sync Policies: A Practical Guide | Codefresh, accessed October 3, 2025, https://codefresh.io/learn/argo-cd/argocd-sync-policies-a-practical-guide/
- Conflict resolution strategies in Data Synchronization | by Mobterest Studio - Medium, accessed October 3, 2025, https://mobterest.medium.com/conflict-resolution-strategies-in-data-synchronization-2a10be5b82bc
- What is Sync? - Stack by Convex, accessed October 3, 2025, https://stack.convex.dev/sync
- Concurrency and automatic conflict resolution - codecentric AG, accessed October 3, 2025, https://www.codecentric.de/en/knowledge-hub/blog/concurrency-and-automatic-conflict-resolution
- Tombstone (data store) - Wikipedia, accessed October 3, 2025, https://en.wikipedia.org/wiki/Tombstone_(data_store)
- Delete data | DataStax Enterprise, accessed October 3, 2025, https://docs.datastax.com/en/dse/6.9/architecture/database-internals/about-deletes.html
- Tombstones | Apache Cassandra Documentation, accessed October 3, 2025, https://cassandra.apache.org/doc/latest/cassandra/managing/operating/compaction/tombstones.html
- Tombstone Record - QuestDB, accessed October 3, 2025, https://questdb.com/glossary/tombstone-record/
- Tombstones - Design Gurus, accessed October 3, 2025, https://www.designgurus.io/course-play/grokking-the-advanced-system-design-interview/doc/tombstones
- How to prevent infinite loops in bi-directional data syncs | Workato ..., accessed October 3, 2025, https://www.workato.com/product-hub/how-to-prevent-infinite-loops-in-bi-directional-data-syncs/
- saveData - Developer Documentation - Obsidian Developer Docs, accessed October 3, 2025, https://docs.obsidian.md/Reference/TypeScript+API/Plugin/saveData
- Plugin - Developer Documentation - Obsidian Developer Docs, accessed October 3, 2025, https://docs.obsidian.md/Reference/TypeScript+API/Plugin
- Type definitions for the latest Obsidian API. - GitHub, accessed October 3, 2025, https://github.com/obsidianmd/obsidian-api
- Interceptors in the Obsidian API - Developers: Plugin & API, accessed October 3, 2025, https://forum.obsidian.md/t/interceptors-in-the-obsidian-api/68434
- Vault - Developer Documentation, accessed October 3, 2025, https://docs.obsidian.md/Reference/TypeScript+API/Vault
- Vault - Developer Documentation, accessed October 3, 2025, https://docs.obsidian.md/Plugins/Vault
- Is it possible to create a file inside .obsidian folder? - Developers: Plugin & API, accessed October 3, 2025, https://forum.obsidian.md/t/is-it-possible-to-create-a-file-inside-obsidian-folder/88072
- Events - Developer Documentation - Obsidian Developer Docs, accessed October 3, 2025, https://docs.obsidian.md/Plugins/Events
- List of all events? - Developers: Plugin & API - Obsidian Forum, accessed October 3, 2025, https://forum.obsidian.md/t/list-of-all-events/93218
- [Feature Request] Conflict Handling to Help with Multi-Device Usage ..., accessed October 3, 2025, Vinzent03/obsidian-git#803
- Robust Sync Conflict Resolution - Help - Obsidian Forum, accessed October 3, 2025, https://forum.obsidian.md/t/robust-sync-conflict-resolution/93544
- How can I access files within my plugin folder - Developers - Obsidian Forum, accessed October 3, 2025, https://forum.obsidian.md/t/how-can-i-access-files-within-my-plugin-folder/89561