Ling Sync is a mobile-safe Obsidian Community Plugin that sends selected Markdown notes to Ling through the provider-neutral Notes Integration API. It uses only public Obsidian APIs, works with isDesktopOnly: false, and does not import Node.js, Electron, or filesystem modules at runtime.
obsidian://ling-sync.SecretStorage.requestUrl.P0 sync is strictly Obsidian to Ling. Every client pairing and connector claim is fixed to the minimum notes.read and notes.sync scopes.
1.11.4 or newer (SecretStorage was added in 1.11.4).SecretStorage, obtains the authoritative server cursor, and uploads a complete manifest.The protocol format is:
obsidian://ling-sync?pairing_id=<id>&pairing_code=<one-time-code>&api_base_url=https%3A%2F%2Fapi.withling.top
api_base_url is optional. The default is https://api.withling.top; the plugin appends /ling-api unless it is already present.
The plugin requires HTTPS so connector tokens are never sent over plaintext. HTTP is accepted only for loopback development hosts: localhost, 127.0.0.1, and [::1].
The claim body is:
{
"pairing_id": "pairing-id",
"pairing_code": "one-time-code",
"vault_id": "stable-vault-uuid",
"vault_name": "My Vault",
"device_id": "stable-device-uuid",
"device_name": "Obsidian Mobile",
"folder_paths": ["Projects/Ling"],
"scopes": ["notes.read", "notes.sync"],
"plugin_version": "0.1.4"
}
An empty folder selection is encoded as [""], which means the whole Vault. The plugin never sends an empty folder_paths array.
The API root is <Ling API base URL>/ling-api. Connector routes are:
POST /integrations/notes/obsidian/connector/claim
POST /integrations/notes/obsidian/connector/refresh
POST /integrations/notes/obsidian/connector/heartbeat
PUT /integrations/notes/obsidian/connector/manifest
POST /integrations/notes/obsidian/connector/changes/batch
P0 always requests the exact provider-neutral scopes notes.read and notes.sync. The plugin has no broader permission option.
The plugin sends a complete manifest as:
{
"idempotency_key": "uuid",
"snapshot_id": "snapshot-uuid",
"chunk_index": 0,
"is_last": true,
"base_cursor": 4,
"next_cursor": 5,
"entries": [
{
"note_id": "stable-note-uuid",
"path": "Projects/Ling/plan.md",
"content": "# Plan\n",
"content_hash": "sha256-hex-of-utf8-content",
"modified_at": "2026-07-17T10:00:00.000Z",
"metadata": {}
}
]
}
Large manifests are split into an ordered snapshot: at most 100 entries and safely below the 8 MiB UTF-8 JSON limit per chunk. Every acknowledged chunk advances the cursor by one; only is_last: true tombstones notes absent from the completed snapshot. A new startup reconciliation safely replaces an interrupted partial snapshot with a new snapshot_id from chunk 0.
Change batches use the same cursor window and a stable idempotency_key for an in-flight retry. Every operation has its own operation_id. Content is included for create and modify; rename sends previous_path; delete sends the deleted path. A batch contains at most 100 operations and stays safely below 8 MiB. Markdown content is measured as UTF-8. If an already mirrored note grows beyond 2 MiB, the plugin sends a delete operation so Ling cannot retain stale content. The plugin keeps an explicit connection error for the oversized path; after the file returns under the limit, a complete manifest restores the current content.
At startup and whenever Obsidian returns to the foreground, heartbeat or token refresh returns the authoritative server cursor and the plugin sends a fresh complete manifest. Focus and visibility events are coalesced so a mobile resume does not upload two manifests. If the plugin stopped after the server committed a request but before local settings were saved, it does not replay the stale cursor. A runtime 409 follows the same reconciliation path.
Only .md files under configured folders are uploaded. These paths are always excluded:
Vault.configDir).trash.git.Vault access uses Vault.getMarkdownFiles() and Vault.cachedRead(). The plugin does not access the filesystem adapter directly.
npm install
npm test
npm run build
npm run check:release
The build produces main.js. For a manual install, place these files in <vault>/.obsidian/plugins/ling-sync/:
main.js
manifest.json
Enable Ling Sync in Obsidian's Community Plugins settings, configure the API and folders, and start pairing from Ling.
manifest.json, package.json, and versions.json must describe the same plugin version and minimum Obsidian version. npm run check:release verifies that contract. A release tag must be the exact semantic version from manifest.json (for example, 0.1.4, without a v prefix).
Pushing an exact version tag runs tests, builds the production bundle, verifies the metadata, and creates a draft GitHub release with the assets Obsidian installs:
main.js
manifest.json
Review and publish the draft before submitting the repository to the Obsidian Community Plugin directory. versions.json stays in the repository root as the compatibility map; it is not a release attachment.
Ling is authoritative for unlinking. API base URL and folders are locked while connected because they are part of the pairing authorization. Unlink the connection in Ling, then pair again to change them; the plugin does not expose a local-only disconnect that could leave an active server mirror behind.
SecretStorage. Tokens are never written to plugin data.Copyright (c) 2026 ZavixAI.
The files in this repository are licensed under the Mozilla Public License 2.0. This license applies only to this plugin repository; Ling services and other separate products are not licensed by this repository. See LICENSE.