Cross-device reading progress, annotations, metadata, and render-settings sync for KOReader.
Syncery synchronises per-device reading data across all your KOReader devices — via Syncthing or cloud storage (Dropbox/WebDAV/FTP).
New here? Start with the Setup & Sync Guide — how to set up Syncthing or cloud sync from scratch.
- Setup & Sync Guide — start here if you're setting things up
- Features
- Supported devices
- Installation
- First-time setup
- Storage modes
- What Syncery syncs
- Menu reference
- Transports
- Conflict resolution
- Lifecycle events
- Translations
- Settings reference
- Architecture overview
- Troubleshooting
- Acknowledgements
- See also
- License
Syncery keeps your reading life in step across every KOReader device — self-hosted, with no account and no third-party server in the middle.
- Reading position — start a book on one device and pick up exactly where you left off on another. When a device is further ahead, Syncery offers to jump you there — your choice of automatic, ask first, or never.
- Highlights, notes & bookmarks — your annotations follow you everywhere. Deleted ones go to a Trash Bin you can restore from.
- Book details — reading status (Reading / On hold / Finished), star rating, collections, your summary note, a custom title or author, and a handmade table of contents all stay in sync.
- Font & layout — optionally sync per-book font and margins too. Off by default, since a size that's comfortable on a phone is wrong on a large e-reader.
- Your files, your transport — everything travels as plain JSON over Syncthing (direct device-to-device) or your own cloud (Dropbox / WebDAV / FTP).
- Nothing lost when devices disagree — edits made on different devices merge instead of overwriting each other. The one case that can truly clash — marking a book Finished on one device and On hold on another — is surfaced for you to resolve, never dropped silently.
- Works offline — changes sync whenever your devices next reconnect; a device that's been offline for weeks keeps its place.
- Whole-library Sync Now — one tap pushes and pulls every book with a pending change, not just the one you have open, and discovers books your other devices have synced that you've never opened here — their progress and annotations are prefetched, so they're ready the moment you do.
- Reading statistics & vocabulary — automatically trigger KOReader's built-in Statistics and Vocabulary Builder plugins to sync their databases over your cloud storage, using the same periodic schedule. Syncery triggers the sync; the plugins handle the merge themselves.
- See everything in one place — a Progress Browser shows how far each device has read in every book (and jumps you to any of them), and an Annotation Browser gathers all your highlights and notes across your whole library.
For the exact fields and toggles, see What Syncery syncs; for how it's built, see Architecture overview.
Any device that runs a reasonably recent version of KOReader
Syncery writes JSON files to disk — it does not manage its own daemon. Data replication is handled by the transport (Syncthing or Cloud), so any device that can run KOReader and one of those transports can participate.
-
Download the latest
syncery.koplugin.zipfrom Releases. -
Extract
syncery.koplugin/into your KOReader plugins directory:Device Plugins directory Kobo /mnt/onboard/.adds/koreader/plugins/Kindle /mnt/us/koreader/plugins/Android /koreader/plugins/ -
Restart KOReader. The plugin appears as Syncery in ☰ → Tools.
Open Syncery → Check for plugin updates. It checks GitHub for a newer release, shows the notes, installs it in place, and offers to restart.
The plugin itself has no runtime dependencies beyond KOReader. Each transport needs its backend:
- Syncthing — a running Syncthing daemon with REST API access (via the KOSyncthing+ plugin, another Syncthing plugin and Android app for Android devices).
- Cloud — KOReader's "Cloud storage+" plugin with a configured service (Dropbox/WebDAV/FTP).
The first-run wizard (triggers automatically on first load) walks through up to 5 steps in a single centred panel:
Pick how this device will sync: Syncthing (peer-to-peer), Cloud (request-response, Dropbox/WebDAV/FTP), or Decide later (save your data locally now and pick a transport afterwards). If KOSyncthing+ is installed, the Syncthing row offers auto-discovery.
If you chose Syncthing without KOSyncthing+, an InputDialog asks for the
Syncthing REST API key. This step is skipped otherwise.
Toggle progress and annotations. Both are off by default — consent-first, so nothing syncs until you turn it on. (Finer controls — metadata, render settings, annotation sub-toggles — live in the menu under What's synced after setup.)
Set a human-readable label that other devices will see (e.g. "Kindle Paperwhite"). Shown in the status panel and in every merge record.
A recap step shows all choices. Tap Done to save and start syncing.
SDR (default) Write:
<book>.sdr/<book>.syncery-annotations.jsonRule: follows KOReader's "Book metadata location" (the book's sidecar directory) Read: canonical location, with a fallback across KOReader's three metadata locations (book folder / docsettings / hashdocsettings) Best for: users who want Syncery to follow their existing KOReader setupSynceryhash Write:
<settings>/syncery/synceryhash/<shard>/<book_md5>/syncery-annotations.jsonRule: content-addressed (same book = same path on every device) Read: single location (no fallback needed) Best for: users who want a known, separate location to syncDon't confuse the two "hash" ideas: Synceryhash is where Syncery keeps its own files. KOReader's hashdocsettings is a separate setting — one of the three places KOReader keeps its native
.sdr, and just one of the locations the SDR mode above follows. You don't need hashdocsettings to use Synceryhash.Both modes share (private, never synced):
last_sync/<book_md5>/— the per-device ancestor used for 3-way mergecloud_staging/— upload stagingsync-journal.ndjson— merge event logsyncery_activity.json— recent-activity recordMigration: run-once, idempotent, moves shared files only. Advanced → Storage mode.
| Category | What gets saved | Merge strategy | UI toggles |
|---|---|---|---|
| Reading progress | Per-device: (percent, page, total_pages, xpath, revision, timestamp, device_id) |
Last-write-wins by (revision, timestamp) |
Master toggle |
| Annotations | Highlights, notes, bookmarks — each with colour, drawer, timestamps, device_id | 3-way merge with tombstone tracking | Master toggle + highlights/notes/bookmarks sub-toggles (each filters its own type; a disabled type stays local and is preserved for other devices in the shared file) + file-extension filter |
| Book metadata | Reading status, rating, collections, summary note, custom title/author, handmade TOC | Per-field last-write-wins; status uses the lifecycle lattice | Master toggle + per-field sub-toggles |
| Render settings | Font face, font size, line spacing, font weight, page margins | Per-field last-write-wins | Master toggle (off by default) + per-field sub-toggles |
| KOReader plugin databases | Syncery does not carry these DBs. It periodically triggers KOReader's Reading Statistics and Vocabulary Builder to run their own cloud-storage sync, so your stats and word lists stay in sync just like your position and annotations. | N/A (Syncery only triggers; the plugins' cloud merge handles conflicts) | Master toggle (Statistics & Vocabulary) + sub-toggles per plugin + interval (1–1440 min) + unified server opt-in |
Syncery can periodically trigger KOReader's native Reading Statistics and Vocabulary Builder to sync their own databases over the cloud.
Syncery does not carry, read, or merge these SQLite databases. Instead, it fires each plugin's existing Cloud storage+ sync on a repeating timer — the same mechanism they use when you tap "Sync now" in their own menus. Each plugin performs its own three-way merge, so concurrent edits across devices are reconciled, not overwritten.
Requirements:
- Cloud storage+ plugin enabled (or the built-in SyncService)
- Each plugin needs a cloud server configured in its own settings (Dropbox or WebDAV — FTP does not work)
- Syncthing does not carry these plugins' databases; cloud storage only
What you configure under Sync Vocab & Statistics:
- Master toggle — on/off (off by default)
- Statistics / Vocabulary sub-toggles — which plugin to trigger
- Interval — how often (1–1440 min, default 5)
- Use Syncery's cloud server — writes Syncery's server address into each plugin's settings so you configure the destination once instead of in each plugin separately
Page-turn behaviour: the actual sync runs synchronously on KOReader's UI thread, so a page turn during sync may lag briefly.
Syncery lives under ☰ → Tools. The full tree:
Menu tree (from the source)
Syncery ← top-level entry in ☰ → Tools
│
├── ⚠ Reading status differs — tap to resolve ← only when the open book's status conflicts across devices
│
├── [smart status header] ← one-line status summary
│ • greyed out when everything is fine
│ • "⚠ … — tap to resolve" when a transport needs attention
│ • rightmost badge: current reading % (when a book is open)
│
├── Sync this book ← per-book sync on/off (when a book is open)
├── Sync now ← trigger a sync right now
│
├── What's synced ▸ ← label adapts live
│ ├── Reading position ← progress on/off
│ ├── ── synced both ways ──
│ ├── [annotations] ── Sync specific types ──: Highlights · Notes · Bookmarks
│ │ + Trash Bin (deleted annotations)…
│ ├── ── Choose which metadata to sync ── + Push this book's handmade TOC
│ ├── Font & layout… ← render settings (── Choose which settings to sync ──)
│ ├── Statistics & Vocabulary ▸ ← auto-trigger for KOReader's native plugins
│ │ ├── Sync Vocab & Statistics ← master toggle
│ │ ├── Statistics ← sub-toggle
│ │ ├── Vocabulary ← sub-toggle
│ │ ├── Use Syncery's cloud server ← point both at the WebDAV server configured on Syncery
│ │ ├── Sync interval: 5 min ← spinner (1–1440)
│ │ └── How this works… ← info dialog
│ ├── ── on this device ──
│ ├── Adapt highlight style to this device
│ └── Jump: Jump automatically / Ask first / Never jump (+ Jump to another device now…)
│
├── Transports ▸
│ ├── Configure Syncthing… ▸ ← Set up API key… · Choose Syncthing folder · Test connection · Advanced (port)
│ └── Cloud settings ▸ ← pick destination · Clear destination · Check cloud settings · debounce window
│
├── Progress Browser ← how far every device has read in each book; jump to any device
├── Annotation Browser ← browse all synced highlights and notes across every book
│
├── This book ▸ ← only when a document is open
│ ├── Undo last jump
│ ├── Delete annotations only (keeps progress)
│ └── Full reset – mark all as deleted (all devices)
│
├── Tools ▸
│ ├── ── Your Syncery data ──
│ ├── Sync pre-Syncery annotations… ← bulk import from KOReader
│ ├── Manage all synced books…
│ ├── Remove orphaned sync files…
│ ├── Recent sync activity
│ ├── Syncthing ▸ ← Rescan all Syncthing folders · KOSyncthing+ integration status…
│ └── Cloud ▸ ← cloud maintenance · Clean cloud upload staging
│
├── Advanced ▸
│ ├── Storage mode: Book metadata folder (SDR) / Synceryhash (+ Migrate all books to this storage mode…)
│ ├── This device: rename · Show QR code
│ ├── Copy diagnostic info
│ ├── Verbose sync logging ← off by default; writes detailed sync events to debug.txt for troubleshooting
│ ├── Book data save interval
│ └── Delete and reset ▸ ← Deep clean – permanently erase all book data… · Reset all Syncery settings…
│
└── Check for plugin updates ← fetch the latest release from GitHub
Syncery writes plain-JSON files; a transport replicates them. Two are supported: Syncthing (peer-to-peer) and Cloud (Dropbox/WebDAV/FTP via KOReader's Cloud storage+ or Cloud storage, immediately consistent). Both implement the same contract — id, push, status — so the orchestrator drives them identically.
Per-transport detail + comparison
Peer-to-peer encrypted file replication via the Syncthing REST API. Configured under Transports → Configure Syncthing…:
- API key — Set up API key… stores the Syncthing REST API key. Not needed when KOSyncthing+ is detected (it supplies the key).
- Folder — Choose Syncthing folder asks the daemon for its folder list and
lets you pick one. That folder is the scan root for book discovery and
.stignoreplacement. - Host — fixed to
127.0.0.1: the daemon runs locally, watching the files Syncery writes. Advanced exposes only the port, for non-standard setups. - Test connection — pings the daemon and validates the API key.
- KOSyncthing+ — when installed, it is auto-detected for URL + API key + folder discovery.
.stignoresuppression —*syncery-*sync-conflict-*is written into.stignoreat the folder root. Non-blocking, idempotent, merge-safe, and works even when the daemon is down.- Eventually consistent — push is fire-and-forget; the daemon replicates asynchronously.
Uses KOReader's Cloud storage+ plugin or if unavailable Cloud storage. Configured under Transports → Cloud settings:
- Destination — pick or change where Syncery uploads its JSON files (Dropbox/WebDAV/FTP, via the chosen backend).
- Clear destination — forget the saved destination on this device only; no cloud files are deleted.
- Check cloud settings — confirms a destination is set. Reachability is verified on the next sync.
- Debounce window — how long to wait after a save before uploading.
- Immediately consistent — upload returns a real HTTP response code.
- Staging + upload — writes to a staging directory; the backend uploads asynchronously, with quiet toast suppression during background sync.
| Aspect | Syncthing | Cloud |
|---|---|---|
| Backend | Syncthing daemon (local) | KOReader SyncService / Cloud storage+ |
| Transport | REST API (HTTP/HTTPS) | Provider-specific (WebDAV, FTP, Dropbox API) |
| Consistency | Eventual (push fire-and-forget) | Immediate (HTTP response) |
| Folder / destination | Pick from the live folder list | Pick the destination via the backend |
| Conflict copies | Possible (.sync-conflict-*) |
Never (request-response) |
| Auth | API key (manual / KOSyncthing+ / config.xml) | Backend credentials |
| .stignore | *syncery-*sync-conflict-* managed |
N/A |
| Scan trigger | REST scan after push | N/A (upload only) |
| Retry | Via Policy (transient errors) | Via Policy (transient errors) |
| Test | Ping the daemon | Destination check (reachability on next sync) |
Every save reads three views — local, the shared remote, and the per-device ancestor — and merges them:
flowchart TD
SAVE[Save event] --> READ[Read local · remote · ancestor]
READ --> MERGE{3-way merge}
MERGE -->|local = remote| SKIP[Skip — nothing changed]
MERGE -->|one side changed| ADOPT[Adopt changed side]
MERGE -->|both changed, same field| CONFLICT[Conflict record]
CONFLICT --> PERSIST[Save to shared + journal]
ADOPT --> PERSIST
PERSIST --> PUSH[Push via transport]
PUSH --> DONE([Done])
Reading status is the one field that can genuinely conflict. KOReader stores it as new / reading / complete / abandoned — the last two appear in the UI as Finished and On hold. Because complete (Finished) and abandoned (On hold) are incomparable, status resolves through a small lattice rather than a timestamp:
stateDiagram-v2
new --> reading
reading --> complete
reading --> abandoned
note right of complete
complete and abandoned
are incomparable —
never silently merge
end note
Per-category detail
Three views are compared on every sync:
- Local — what's on this device right now
- Remote — what's in the shared JSON file
- Ancestor — the last-sync file recorded per-device
The 3-way merge (Git-style, in merge.lua) distinguishes:
- In last-sync + NOT in local → the user deleted it. Keep it deleted.
- NOT in last-sync + NOT in local → new from remote. Adopt it.
- Same field changed on both sides → a conflict record. Data preserved, not lost.
Each entry carries (revision, timestamp). Highest revision wins;
the timestamp breaks ties. No entry is ever structurally deleted.
The status lattice (status_lattice.lua) handles the special case:
new < reading < complete(forward, automatic)new < reading < abandoned(forward, automatic)complete ⟂ abandoned(incomparable — resolved by the generation counter)
When Syncthing creates *syncery-*sync-conflict-* files:
.stignoreprevents replication to other devices.- The conflict resolver (2-way merge:
(revision, timestamp)wins for progress; per-entry newer-wins for annotations) merges the content and deletes the copy. - KOSyncthing+ hides them from its conflict badge.
The Cloud transport does not produce conflict copies (request-response).
KOReader's lifecycle events (close, suspend, resume, power-off, quit, settings flush) are routed to a central teardown with the right "how hard to flush" opts — strongest on quit and close (which also shut the transport down), flush-only on suspend and power-off. Stale timers are guarded by a destroyed flag.
Event-to-handler table
| KOReader event | Lifecycle handler | Teardown opts | What happens |
|---|---|---|---|
onCloseDocument |
on_close_document() |
{ destroying = true } |
Full destroying flush: persist progress + annotations + opportunistic cloud push + deferred scan; transport shut down |
onSuspend |
on_suspend() |
{} |
Flush only — no teardown |
onResume |
on_resume() |
(none) | Bounded connectivity poll, then a checkRemote re-check on wake |
onPowerOff |
on_power_off() |
{} |
Flush only (non-destroying — the plugin survives, the transport stays up) |
onQuit |
on_quit() |
{ destroying = true } |
Strongest: full destroying flush + transport shutdown, at process scope |
onFlushSettings |
on_flush_settings() |
(none) | Debounce-triggers a Syncthing scan when a document is open |
onSaveSettings |
on_save_settings() |
(none) | Persists Syncery state alongside KOReader's settings flush |
onPageUpdate / onPosUpdate |
schedule_auto_save() |
(none) | Arms the autosave timer |
All lifecycle callbacks check a destroyed flag before executing, so stale
timers from a closed document never fire.
| Language | File | Status |
|---|---|---|
| Bulgarian | locale/bg.po |
Full |
| English (source) | locale/syncery.pot |
Master template |
The template is generated from _("…") and _n("…", "…", n) calls in the Lua
source. Workflow:
python3 tools/i18n.py sync # update .pot + .po from source
python3 tools/i18n.py check # verify consistencySee tools/README.md for details.
All settings persist in KOReader's G_reader_settings under syncery_* keys:
syncery_* keys in KOReader's G_reader_settings
| Key | Type | Default | Description |
|---|---|---|---|
syncery_storage_mode |
string | "sdr" |
"sdr" (Book metadata folder) or "hash" (Synceryhash) |
syncery_use_syncthing |
bool | false |
Master toggle for Syncthing |
syncery_syncthing_api_key |
string | — | Syncthing REST API key |
syncery_syncthing_folder_id |
string | — | Chosen Syncthing folder ID |
syncery_syncthing_folder |
table | — | { folder_id, path, label } record |
syncery_syncthing_port |
number | daemon default | Syncthing GUI port (host fixed to 127.0.0.1) |
syncery_syncthing_scheme |
string | "http" |
"http" or "https" for the Syncthing GUI |
syncery_use_cloud |
bool | false |
Master toggle for Cloud |
syncery_cloud_server |
table | — | Cloud destination descriptor |
syncery_cloud_upload_delay |
number | 60 |
Debounce (seconds) before a cloud upload; min 15 |
syncery_db_sync_enabled |
bool | false |
Master toggle for Statistics & Vocabulary auto-trigger |
syncery_db_sync_stats |
bool | true |
Include Reading Statistics in the auto-trigger |
syncery_db_sync_vocab |
bool | true |
Include Vocabulary Builder in the auto-trigger |
syncery_db_sync_unify |
bool | false |
Point both plugins at Syncery's cloud server (WebDAV/Dropbox only) |
syncery_db_sync_interval_min |
number | 5 |
Interval (minutes) between auto-triggers |
syncery_sync_progress |
bool | false |
Toggle progress sync |
syncery_sync_annotations |
bool | false |
Toggle annotation sync |
syncery_sync_highlights |
bool | true |
Highlights sub-toggle |
syncery_sync_notes |
bool | true |
Notes sub-toggle |
syncery_sync_bookmarks |
bool | true |
Bookmarks sub-toggle |
syncery_sync_extensions |
table | all | File-extension filter |
syncery_sync_metadata |
bool | false |
Toggle metadata sync |
syncery_sync_status |
bool | true |
Reading status sub-toggle |
syncery_sync_rating |
bool | true |
Rating sub-toggle |
syncery_sync_collections |
bool | true |
Collections sub-toggle |
syncery_sync_summary |
bool | true |
Summary note sub-toggle |
syncery_sync_custom_metadata |
bool | true |
Custom title/author sub-toggle |
syncery_sync_handmade_toc |
bool | true |
Handmade TOC sub-toggle |
syncery_sync_render_settings |
bool | false |
Master toggle for render settings |
syncery_sync_font_face |
bool | false |
Font face sub-toggle |
syncery_sync_font_size |
bool | false |
Font size sub-toggle |
syncery_sync_line_spacing |
bool | false |
Line spacing sub-toggle |
syncery_sync_font_weight |
bool | false |
Font weight sub-toggle |
syncery_sync_margins |
bool | false |
Page margins sub-toggle |
syncery_jump_mode |
string | "ask" |
"auto", "ask", "never" |
syncery_adapt_highlight_style |
bool | true |
Adapt highlight style to this device |
syncery_min_save_interval |
number | 5 |
Autosave interval floor (seconds); range 5–120 |
syncery_tombstone_ttl_days |
number | 90 |
Days a tombstone keeps its fields before compaction |
syncery_progress_freshness_days |
number | 90 |
Progress freshness window (days); range 7–365 |
syncery_device_label |
string | hostname | Human-readable device name |
syncery_device_id |
string | fallback | Self-generated id, used only when KOReader's device_id is unavailable |
syncery_firstrun_done |
bool | — | First-run wizard completed |
syncery_debug_logging |
bool | false |
Verbose sync logging to debug.txt (Advanced) |
The jump thresholds (percent_epsilon, sync_trigger_delta) are internal
jump_policy constants, not persisted settings. A few additional internal keys
exist (e.g. journal/activity-log caps). Transport configuration lives in
G_reader_settings — plugin updates never touch your credentials or pairing
data. Syncery's device identity is KOReader's own device_id (a stable UUID it
generates once at startup and also uses to stamp native annotations);
syncery_device_id is only a fallback for environments where that is unset.
The plugin is organised into independent engines (syncery_ann/, syncery_progress/), a transport layer (syncery_transports/ with syncthing/ and cloud/ providers), lifecycle and timers (syncery_lifecycle/), the UI (syncery_ui/), and storage-mode migration (syncery_migration/). Pure logic (merge, policy, identity, lattice) is kept separate from I/O so it can be unit-tested.
Full module tree
syncery.koplugin/
├── _meta.lua Plugin manifest
├── main.lua WidgetContainer, ~90 methods
├── insert_menu.lua Splices into KOReader's Tools menu
├── syncery_i18n.lua Gettext loader
├── syncery_settings.lua G_reader_settings I/O
├── syncery_storage_mode.lua SDR/hash singleton, listener callbacks
├── syncery_update.lua Self-update from GitHub releases
├── syncery_util.lua ensure_dir, move_file
│
├── syncery_ann/ Annotation engine
│ ├── merge.lua 3-way merge (L / R / A)
│ ├── sync_orchestrator.lua Drives the annotation sync sequence
│ ├── paths.lua Shared + last-sync path computation
│ ├── state_store.lua On-disk JSON shape
│ ├── json_store.lua Atomic JSON I/O
│ ├── doc_settings_bridge.lua KOReader ↔ Syncery keyed map
│ ├── metadata_bridge.lua Per-field metadata merge
│ ├── render_settings_bridge.lua Per-field render settings merge
│ ├── status_lattice.lua Clock-free status resolution
│ ├── conflict_resolver.lua 2-way merge for conflict copies
│ ├── tombstones.lua TTL compaction, never drops the marker
│ ├── identity.lua XPointer / page+coords identity
│ ├── mtime_gate.lua Mtime-based sync debounce
│ ├── time_format.lua Timestamp formatting / parsing
│ ├── bulk_ingest.lua One-time native annotation import
│ └── hash_location_finder.lua Walk KOReader's hash docsettings tree
│
├── syncery_progress/ Progress engine
│ ├── merge.lua LWW per device_id
│ ├── sync_orchestrator.lua Drives the progress sync sequence
│ ├── paths.lua Shared + last-sync + journal paths
│ ├── state_store.lua On-disk JSON shape
│ ├── conflict_resolver.lua 2-way merge for conflict files
│ ├── progress_bridge.lua KOReader live state → entry
│ ├── sync_journal.lua Append-only NDJSON log
│ └── scan_target.lua Narrow Syncthing scan sub-path
│
├── syncery_transports/ Transport layer
│ ├── init.lua Factory + orchestrator + bridge
│ ├── interface.lua Contract: id, push, status
│ ├── orchestrator.lua Transport list, per-book state, Policy
│ ├── bridge.lua main.lua facade
│ ├── policy.lua classify_error, retry delay
│ ├── http_client.lua Single-request HTTP client
│ ├── stignore.lua .stignore writer
│ ├── safe_callback.lua Exactly-once callback guard
│ ├── plugin_sync.lua Shared push/pull helpers
│ ├── log.lua Transport logging
│ ├── syncthing/ Syncthing transport
│ │ ├── transport.lua Implementation
│ │ ├── folder_discovery.lua Folder-list parsing
│ │ ├── kosyncthing_plus_api_client.lua KOSyncthing+ bridge
│ │ ├── local_url.lua Host / port / scheme assembly
│ │ └── providers/ manual · kosyncthing_plus · config_xml
│ └── cloud/ Cloud transport
│ ├── transport.lua Implementation
│ ├── staging.lua Staging-directory upload
│ ├── sync_service_adapter.lua SyncService bridge
│ ├── quiet_toast.lua Background-sync toast suppression
│ └── providers/ syncservice · cloudstorage
│
├── syncery_lifecycle/ Event routing + timers
│ ├── init.lua Lifecycle dispatcher
│ ├── timers.lua Slot-keyed scheduling
│ ├── teardown.lua Central flush sequence
│ └── wifi_backoff.lua Exponential-backoff retry
│
├── syncery_ui/ User interface
│ ├── menu/ Menu tree (section files)
│ ├── booklist/ Manage-all-books list
│ ├── progress_browser/ Cross-device reading-progress dashboard
│ ├── annotation_viewer/ Cross-device highlights/notes browser
│ ├── status_ui/ Status detail view + device picker
│ ├── trash/ Delete/restore browser
│ ├── wizard.lua / _presenter / _window First-run wizard
│ ├── status_panel.lua Status panel view
│ ├── status_section.lua Smart header + status rows
│ ├── notify.lua 3-tier notifications
│ ├── toast_widget.lua E-ink toast widget
│ ├── jump_policy.lua Jump decision (pure)
│ ├── jump_toast.lua Jump invitation / undo bar
│ ├── diagnostic_snapshot.lua 3-tier snapshot
│ └── action_bar.lua Non-blocking bottom bar
│
└── syncery_migration/ Storage-mode migration
├── storage_mode.lua Run-once bidirectional
├── orphan_cleanup.lua kept/orphan/fail-closed
├── orphan_adapters.lua Real-world adapter layer
└── scattered_metadata.lua Read-only detection
Common problems and how to fix them:
Common problems and solutions
One or more transports are enabled but their backends are not reachable.
- Syncthing: Is the daemon running? Check
http://127.0.0.1:8384. If using KOSyncthing+, open its menu to verify daemon status. Tap the header → opens the failing transport's setup. - Cloud: Is KOReader's SyncService or Cloud storage+ configured with a working destination? Check with Transports → Cloud settings → Check cloud settings.
Syncthing is enabled but has no API key or folder:
- Open Transports → Configure Syncthing…, set the API key, pick a folder.
- Or install KOSyncthing+ for auto-discovery.
- Syncthing: Eventually consistent. Push is fire-and-forget; the daemon replicates asynchronously. Wait a few seconds, trigger a rescan on the receiving device. Check the Syncthing log for folder errors.
- Cloud: Request-response. If upload returned 200/201, the file is on the server. Check the receiving device's Cloud status.
- Debounce: Syncery debounces pushes per-transport per-book. Rapid saves within the window are skipped.
- Jump mode is
"never"— change it under What's synced → (on this device) → Jump → Ask first. - No remote data yet — the other device hasn't synced this book.
- The percent difference is below the threshold.
- It's too soon since the last local write.
- Deleted annotations become tombstones. Delete some via This book → Delete annotations only (keeps progress) or Full reset, then browse the Trash Bin (under What's synced).
- The Trash Bin is per-book (open the book, then open the Trash Bin).
- Every annotation-merge event is recorded. "skip" means the merge was debounced (no change). "conflict_absorbed" means a conflict file was merged and cleaned. "failed" means something went wrong — check the transport's maintenance under Tools → Syncthing / Tools → Cloud.
- Re-open Advanced → Storage mode and confirm. Migration is idempotent.
- Run Tools → Remove orphaned sync files… to find stray files.
- Check that
.stignorecontains*syncery-*sync-conflict-*. A sync run rewrites it. - If using KOSyncthing+, verify its integration status under Tools → Syncthing → KOSyncthing+ integration status….
- The Cloud transport does not produce conflict copies.
- Restart KOReader (plugins load at startup).
- Verify the folder is named exactly
syncery.koplugin/in the rightplugins/directory. - Check KOReader's
crash.logfor Syncery errors.
The Annotation Browser is inspired by xblain/annotationsviewer.koplugin — the idea of browsing annotations across all books in a single view was pioneered there. Syncery's version extends that concept to show annotations from every device side by side.
d0nizam/kosyncthing_plus.koplugin - KOSyncthing+ is a KOReader plugin that embeds a fully managed Syncthing daemon right inside your e-reader.
AGPL-3.0 — see LICENSE
Licensed under the same terms as KOReader itself.
Copyright © 2026 d0nizam (https://github.com/d0nizam)