{"id":51261185,"url":"https://github.com/richardcarls/idb-free-sync","last_synced_at":"2026-06-29T12:01:42.563Z","repository":{"id":366004301,"uuid":"1269607495","full_name":"richardcarls/idb-free-sync","owner":"richardcarls","description":"idb-free-sync is a browser-focused TypeScript library for synchronizing IndexedDB object stores with cloud-backed JSON files.","archived":false,"fork":false,"pushed_at":"2026-06-19T21:25:13.000Z","size":791,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-19T21:25:26.528Z","etag":null,"topics":["indexeddb-wrapper","local-first","serverless","synchronization"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/richardcarls.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-14T23:22:09.000Z","updated_at":"2026-06-19T21:25:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/richardcarls/idb-free-sync","commit_stats":null,"previous_names":["richardcarls/idb-free-sync"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/richardcarls/idb-free-sync","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/richardcarls%2Fidb-free-sync","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/richardcarls%2Fidb-free-sync/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/richardcarls%2Fidb-free-sync/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/richardcarls%2Fidb-free-sync/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/richardcarls","download_url":"https://codeload.github.com/richardcarls/idb-free-sync/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/richardcarls%2Fidb-free-sync/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34925718,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-29T02:00:05.398Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["indexeddb-wrapper","local-first","serverless","synchronization"],"created_at":"2026-06-29T12:01:41.472Z","updated_at":"2026-06-29T12:01:42.558Z","avatar_url":"https://github.com/richardcarls.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @rcarls/idb-free-sync\n\n`@rcarls/idb-free-sync` is a browser-focused TypeScript library for synchronizing IndexedDB\nobject stores with cloud-backed JSON files.\n\nIt provides:\n\n- A small `SyncTransport` interface for storage providers\n- A `syncStore` orchestrator for syncing an `idb` object store\n- Built-in transports for Google Drive, OneDrive, Dropbox, and WebDAV\n- `NullTransport` for disabling sync without special-case application logic\n- Custom conflict resolution and soft-delete support\n- `blobFields` for syncing binary assets alongside records\n- `OPFSBlobStore` for local OPFS-backed blob storage\n\n## Installation\n\n```sh\nyarn add @rcarls/idb-free-sync idb\n```\n\n`idb` is a peer dependency. Cloud provider SDKs are bundled dependencies.\n\nThe package is ESM-only. Import from `@rcarls/idb-free-sync/core` when an application\nprovides its own transport, or import a provider directly to avoid loading\nunrelated provider SDKs:\n\n```ts\nimport { syncStore } from '@rcarls/idb-free-sync/core';\nimport { GoogleDriveTransport } from '@rcarls/idb-free-sync/google';\n```\n\n## Quick Start\n\nRecords must use string primary keys. By default, records should also have a\n`modified` date so `syncStore` can decide whether the local or remote copy is\nnewer. If your schema stores the timestamp under a different name, set\n`modifiedField` (see [Conflict Resolution](#conflict-resolution)).\n\n```ts\nimport { openDB } from 'idb';\nimport { syncStore, type SyncTransport } from '@rcarls/idb-free-sync';\n\ntype Note = {\n  id: string;\n  title: string;\n  modified: Date;\n};\n\nconst db = await openDB('notes-app', 1, {\n  upgrade(database) {\n    database.createObjectStore('notes', { keyPath: 'id' });\n  },\n});\n\n// Configure one of the built-in cloud transports for your provider.\ndeclare const transport: SyncTransport;\n\nawait syncStore\u003cNote\u003e(db, transport, 'notes');\n```\n\nFor a record with the primary key `abc`, the transport stores a file named\n`abc.json` inside the `notes` store directory.\n\n## Conflict Resolution\n\nThe default resolver compares `local.modified` with the remote file's modified\ntimestamp:\n\n- Remote soft deletion deletes the local record.\n- If only one side has a modified timestamp, that side wins.\n- The newer side wins.\n- Equal timestamps are ignored.\n\nIf your schema stores the modification timestamp under a different key, set\n`modifiedField` instead of writing a full custom resolver:\n\n```ts\nawait syncStore(db, transport, 'notes', {\n  modifiedField: 'updatedAt',\n});\n```\n\nProvide a custom resolver when your data needs different behavior:\n\n```ts\nawait syncStore(db, transport, 'notes', {\n  resolve(local, remote) {\n    if (remote.deleted) return 'delete';\n    return local.pinned ? 'keep-local' : 'keep-remote';\n  },\n});\n```\n\nA resolver returns one of:\n\n```ts\ntype ConflictResolution = 'keep-local' | 'keep-remote' | 'delete' | 'ignore';\n```\n\n`syncStore` uploads local-only records and downloads remote-only records. Queue\noperations run in parallel. Individual queue failures are logged rather than\ncausing `syncStore` to reject.\n\n## Soft Deletes\n\nCloud transports support soft deletion by rewriting a remote JSON object with\n`deleted: true`. To avoid restoring a downloaded soft-deleted value locally,\nidentify the corresponding record field:\n\n```ts\nawait syncStore(db, transport, 'notes', {\n  softDeleteField: 'deleted',\n});\n```\n\nThe default resolver recognizes soft deletion only when the transport exposes\nit through `SyncFileInfo.deleted`. Currently, Google Drive exposes that metadata\nduring listing; other built-in cloud transports store the marker in file\ncontent.\n\n## Transports\n\n### Google Drive\n\n```ts\nconst transport = new GoogleDriveTransport(googleOAuthClientId);\n```\n\nThe host page must load Google Identity Services and the Google API client so\nthe global `google` and `gapi` objects are available. Files are stored in the\nGoogle Drive application data folder. An optional `syncUserId` value in\n`localStorage` is used as the OAuth login hint.\n\nRequired scopes are available from `transport.scopes`.\n\n### OneDrive\n\n```ts\nconst transport = new OneDriveTransport(microsoftApplicationClientId);\n```\n\nUses MSAL browser authentication and Microsoft Graph. Configure the application\nredirect URI to match `window.location.origin`. Files are stored in the\napplication folder.\n\n### Dropbox\n\n```ts\nlocalStorage.setItem('dropboxAccessToken', accessToken);\nconst transport = new DropboxTransport();\n```\n\nUses the access token from `localStorage.dropboxAccessToken`. Files are stored\nunder `/Apps/RecipeTome`.\n\n### WebDAV\n\n```ts\nconst transport = new WebDAVTransport({\n  url: 'https://cloud.example.com/remote.php/dav/files/user',\n  username: 'user',\n  password: 'app-password',\n});\n```\n\nBearer token authentication is also supported:\n\n```ts\nconst transport = new WebDAVTransport({ url, token });\n```\n\nFiles are stored under `/RecipeTome`.\n\n### No Sync\n\n```ts\nconst transport = new NullTransport();\n```\n\n`NullTransport` implements the interface without persisting anything.\n\n## Blob Fields\n\nWhen records reference binary assets — images captured by the app, attachments,\naudio clips — `blobFields` lets you sync those binaries alongside the JSON\nrecords using the same transport.\n\n### Local storage: OPFSBlobStore\n\n`OPFSBlobStore` stores blobs in the Origin Private File System, keyed by a\nstable identifier (typically a content hash). A Service Worker can intercept\nURL requests and serve blobs from OPFS, making them usable as `\u003cimg src\u003e` or\n`\u003caudio src\u003e` values.\n\n```ts\nimport { OPFSBlobStore } from '@rcarls/idb-free-sync';\n\nconst imageStore = new OPFSBlobStore('recipe-images');\n\n// Store a captured blob\nawait imageStore.put('sha256-abc123', capturedBlob);\n\n// Check existence\nconst exists = await imageStore.has('sha256-abc123');\n```\n\n### Service Worker integration (app-side)\n\n```ts\nself.addEventListener('fetch', (event) =\u003e {\n  const url = new URL(event.request.url);\n\n  if (url.pathname.startsWith('/_cache/')) {\n    const key = url.pathname.slice('/_cache/'.length);\n\n    event.respondWith(\n      navigator.storage\n        .getDirectory()\n        .then((root) =\u003e root.getDirectoryHandle('recipe-images'))\n        .then((dir) =\u003e dir.getFileHandle(key))\n        .then((fh) =\u003e fh.getFile())\n        .then((file) =\u003e new Response(file))\n        .catch(() =\u003e new Response('Not found', { status: 404 })),\n    );\n  }\n});\n```\n\n### Syncing blobs with records\n\nConfigure `blobFields` in `syncStore` to sync blobs alongside JSON records. The\ntransport must implement `BlobSyncTransport` — all built-in transports do.\n\nRemote JSON stores the raw blob key; `keyFromValue` and `valueFromKey` map\nbetween that key and the local field value (such as a `/_cache/` URL). This\nkeeps remote files transport-agnostic.\n\nBlobs are stored in a sibling directory named `\u003cstoreName\u003e-blobs/` so they do\nnot appear in the record listing.\n\n```ts\nimport { OPFSBlobStore, syncStore } from '@rcarls/idb-free-sync';\n\ntype Recipe = {\n  id: string;\n  name: string;\n  imageUrl?: string; // '/_cache/\u003chash\u003e' locally, '\u003chash\u003e' in remote JSON\n  modified: Date;\n};\n\nconst imageStore = new OPFSBlobStore('recipe-images');\n\nawait syncStore\u003cRecipe\u003e(db, transport, 'recipes', {\n  modifiedField: 'modified',\n  softDeleteField: 'deleted',\n  blobFields: {\n    imageUrl: {\n      blobStore: imageStore,\n      keyFromValue: (url) =\u003e url.replace('/_cache/', ''), // '/_cache/abc' → 'abc'\n      valueFromKey: (key) =\u003e `/_cache/${key}`, // 'abc' → '/_cache/abc'\n      contentType: 'image/jpeg',\n    },\n  },\n});\n```\n\n**Upload path:** For each record being uploaded, any blob referenced by a\n`blobFields` field is pushed to the transport (skipped if already present\nremotely). The record's field value is replaced with the raw key in remote JSON.\n\n**Download path:** For each record being downloaded, any blob key referenced in\na `blobFields` field is fetched from the transport and stored locally (skipped\nif already present). The field value is rewritten to the local URL before the\nrecord is written to IDB.\n\n**Conflict resolution:** Blob conflict resolution is implicit — the same key\nmeans the same content, so blobs are never merged. The record's conflict\nresolution (keep-local, keep-remote, etc.) determines which blobs move.\n\n### Implementing BlobSyncTransport\n\nTo add blob support to a custom transport, implement `BlobSyncTransport`:\n\n```ts\nimport type { BlobSyncTransport, SyncFileInfo } from '@rcarls/idb-free-sync';\n\nclass CustomTransport implements BlobSyncTransport {\n  // ... SyncTransport methods ...\n\n  async putBlob(\n    storeName: string,\n    blobKey: string,\n    blob: Blob,\n    contentType?: string,\n  ): Promise\u003cSyncFileInfo\u003e {\n    // Upload blob to \u003cstoreName\u003e-blobs/\u003cblobKey\u003e\n  }\n\n  async getBlob(storeName: string, blobKey: string): Promise\u003cBlob | undefined\u003e {\n    // Download blob from \u003cstoreName\u003e-blobs/\u003cblobKey\u003e\n  }\n\n  async listBlobs(storeName: string): Promise\u003cSyncFileInfo[]\u003e {\n    // List all blobs in \u003cstoreName\u003e-blobs/\n  }\n\n  async deleteBlob(storeName: string, blobKey: string): Promise\u003cvoid\u003e {\n    // Delete blob at \u003cstoreName\u003e-blobs/\u003cblobKey\u003e\n  }\n}\n```\n\nUse `isBlobSyncTransport(transport)` to check whether a transport supports\nblob sync at runtime.\n\n## Roadmap\n\nA user-visible device-folder transport could use the File System Access API,\nbut it would require the user to select a directory and grant permissions\nthrough an interactive browser flow. It is intentionally treated as a possible\nexport or local-folder feature rather than transparent synchronization.\n\n## Implementing a Transport\n\nImplement `SyncTransport` to add another provider:\n\n```ts\nimport type { SyncFileInfo, SyncTransport } from '@rcarls/idb-free-sync';\n\nclass CustomTransport implements SyncTransport {\n  readonly provider = 'custom';\n  readonly scopes: string[] = [];\n\n  list(storeName: string): Promise\u003cSyncFileInfo[]\u003e {\n    throw new Error('Not implemented');\n  }\n\n  get\u003cT\u003e(storeName: string, syncKey: string): Promise\u003cT | undefined\u003e {\n    throw new Error('Not implemented');\n  }\n\n  put\u003cT\u003e(storeName: string, syncKey: string, value: T): Promise\u003cSyncFileInfo\u003e {\n    throw new Error('Not implemented');\n  }\n\n  delete(storeName: string, syncKey: string, soft?: boolean): Promise\u003cvoid\u003e {\n    throw new Error('Not implemented');\n  }\n\n  deleteAll(storeName: string, soft?: boolean): Promise\u003cvoid\u003e {\n    throw new Error('Not implemented');\n  }\n\n  count(storeName: string): Promise\u003cnumber\u003e {\n    throw new Error('Not implemented');\n  }\n}\n```\n\nTransport values must be JSON-serializable. `syncKey` values are file names,\nnormally `\u003cprimary-key\u003e.json`.\n\n## Development\n\nThis repository uses Yarn 4 with Plug'n'Play.\n\n```sh\nyarn install\nyarn check\n```\n\n`yarn build` creates ESM, CommonJS, source map, and declaration outputs in\n`dist/`. See [TESTING.md](./TESTING.md) for test commands, coverage policy, and\ncloud-provider testing guidance.\n\n## Contributing\n\nContributions are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup,\ncommit conventions, and pull request guidance. Please report security issues\naccording to [SECURITY.md](./SECURITY.md).\n\n## License\n\n[MIT](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frichardcarls%2Fidb-free-sync","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frichardcarls%2Fidb-free-sync","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frichardcarls%2Fidb-free-sync/lists"}