A privacy-first web and mobile app that lets JW Library users merge and sync their annotations, notes, bookmarks, highlights, tags, and playlists across multiple devices β without losing any data.
JW Library allows exporting your data as a .jwlibrary backup file. But importing a backup overwrites all existing data on the target device. If you use multiple devices (phone, tablet, PC), you're forced to choose one device's data over another.
JW Notes Sync lets you:
- Import multiple
.jwlibrarybackup files - Preview & compare the data from each device side by side
- Merge them intelligently β keeping all unique notes, bookmarks, highlights, and tags from every device
- Resolve conflicts when the same item was edited differently on two devices
- Export a single merged
.jwlibraryfile ready to import on any device - Sync automatically via Google Drive or iCloud (optional, Phase 3)
flowchart LR
A["π± Device A\n.jwlibrary"] --> IMPORT[Import]
B["π± Device B\n.jwlibrary"] --> IMPORT
IMPORT --> MERGE["βοΈ Merge Engine"]
MERGE --> TRUTH["β Source of Truth\n(saved locally)"]
TRUTH --> DL["π₯ Download\nmerged file"]
On your first visit, import two or more .jwlibrary backup files. The app merges them and saves the result as your source of truth β a single file that contains everything from all your devices.
flowchart LR
NEW["π± New backup\n.jwlibrary"] --> MERGE["βοΈ Auto-merge"]
TRUTH["β Current\nSource of Truth"] --> MERGE
MERGE --> NEW_TRUTH["β New\nSource of Truth"]
NEW_TRUTH --> DL["π₯ Download"]
Each time you return with a new backup, just add it. The app automatically merges it against your existing source of truth, producing an updated version.
flowchart TD
subgraph LIBRARY["π Your Library (stored locally)"]
SOT["β Source of Truth\nLatest merged backup"]
O1["π± Google Pixel 6a"]
O2["π± iPad"]
O3["π± Galaxy Tab"]
end
ADD["+ Add a backup"] -->|"new file"| AUTO["Auto-merge\nwith β"]
AUTO --> SOT
QM["β‘ Quick merge"] -->|"pick 2 files"| STANDALONE["One-off merge\n(not saved)"]
Your library persists across sessions using OPFS/IndexedDB. The source of truth cannot be deleted. You can also do standalone "quick merges" without affecting your library.
flowchart TD
subgraph BROWSER["Browser Storage"]
OPFS["OPFS\n(raw .jwlibrary bytes)"]
IDB["IndexedDB\n(metadata + config)"]
LS["localStorage\n(theme, language, onboarding)"]
end
OPFS -->|"fallback"| IDB
All data stays in your browser. Nothing is sent to any server.
- No backend. Zero. Nothing is sent to any server we control.
- Offline-first. The entire app runs in your browser or on your device.
- Your data stays yours. Cloud sync uses private app-scoped storage (Google Drive AppData / iCloud CloudKit) β only your own account can access it.
- Open source. You can verify exactly what the code does.
| Layer | Technology |
|---|---|
| Shared core | Pure TypeScript (packages/core) |
| Web app | SvelteKit |
| Mobile app | React Native (Expo) |
| SQLite (browser) | sql.js (WASM) |
| SQLite (mobile) | expo-sqlite |
| Archive handling | JSZip (web) / react-native-zip-archive (mobile) |
| UI (web) | Tailwind CSS 4 + Bits UI |
| State (web) | Svelte stores |
| State (mobile) | Zustand |
| Cloud sync | Google Drive API v3 (DRIVE_APPDATA) / CloudKit JS |
| Local persistence | OPFS + IndexedDB fallback (web) / expo-file-system (mobile) |
File import/export, database parsing, merge logic, conflict resolution, merged file export.
Visual diff, annotation browser, tag manager, search, statistics dashboard.
Google Drive AppData sync, iCloud CloudKit sync, automatic background sync.
React Native (Expo) app for Android & iOS, native file picking, share-sheet integration.
See the full Project Plan for detailed breakdowns.
# Install dependencies
pnpm install
# Start web dev server
pnpm --filter @jw-notes-sync/web dev
# Start mobile dev server
pnpm --filter @jw-notes-sync/mobile start
# Run core tests
pnpm --filter @jw-notes-sync/core test
# Build web for production
pnpm --filter @jw-notes-sync/web buildThe app supports English (default) and French, with a shared translation system across both platforms.
packages/i18n/ # Shared message source
βββ src/locales/fr.json # French translations (~70 keys)
βββ src/locales/en.json # English translations
βββ src/index.ts # Exports messages, types, locale config
Both apps import @jw-notes-sync/i18n for translation strings and use i18next as the runtime:
| Platform | Libraries | Detection |
|---|---|---|
| Web | i18next + Svelte 5 reactive t() |
navigator.language |
| Mobile | i18next + react-i18next |
expo-localization |
- On startup, the app detects the device/browser language
- If the user previously chose a language manually (in Settings), that preference is restored from storage (
localStorageon web,AsyncStorageon mobile) - When the app regains focus (tab visibility change on web, foreground on mobile), the device language is re-checked β but only if the user hasn't set a manual preference
- Create
packages/i18n/src/locales/{code}.json(copyfr.jsonas template) - Add the locale code to
supportedLocalesinpackages/i18n/src/index.ts - Register the resource in both
apps/web/src/lib/i18n.svelte.tsandapps/mobile/src/lib/i18n.ts - Add the language label to the Settings screen on both platforms
A .jwlibrary file is a DEFLATE-compressed ZIP archive containing:
manifest.jsonβ metadata (device name, date, schema version, DB hash)userData.dbβ SQLite database (schema v14) with 18 tables, 13 indexes, and 23 triggers- Media files β images and thumbnails referenced by playlists
| Table | Purpose | Merge Key |
|---|---|---|
Note |
User notes with title/content | Guid |
UserMark |
Highlights/underlines | UserMarkGuid |
Bookmark |
Saved locations | PublicationLocationId + Slot |
Tag |
User-created tags/folders | Type + Name |
TagMap |
Tag-to-item associations | Composite FK |
Location |
Publication/chapter references | Natural key (Book+Chapter+KeySymbol+Lang+Type) |
BlockRange |
Highlight text ranges | UserMarkId parent |
InputField |
Form field values | LocationId + TextTag |
PlaylistItem |
Media playlist entries | Label + media refs |
JW Library on Android is strict about the archive format. The generated file must have:
PRAGMA user_version = 14andjournal_mode = delete(not WAL)- All 13 indexes and 23 triggers present in the schema
- DEFLATE-compressed ZIP (not STORE)
creationDateas date-only (YYYY-MM-DD),namewith.jwlibraryextension- Correct SHA-256 hash of the raw database bytes in the manifest
See the full Technical Notes in PLAN.md for details.
MIT