{"_id":"@a16n/pact-client","_rev":"3-5dcd3594eef821c3236b7d30292ffd91","name":"@a16n/pact-client","dist-tags":{"latest":"1.2.0"},"versions":{"1.0.0":{"name":"@a16n/pact-client","version":"1.0.0","license":"UNLICENSED","_id":"@a16n/pact-client@1.0.0","maintainers":[{"name":"halcyon400","email":"alex@a16n.dev"}],"dist":{"shasum":"0b7f51b519ebd80d7d03a79af955b3fb194035fa","tarball":"https://registry.npmjs.org/@a16n/pact-client/-/pact-client-1.0.0.tgz","fileCount":5,"integrity":"sha512-kVJnzo3y2OTs9DdD5BoP+X9Q0wEabPFAhtUjJbHOLLAG6SAS2eKVKXnbZqwfcC3lvHngm1aYJ4hboklJZlWxMw==","signatures":[{"sig":"MEUCIQDKTli2BqkJman+nvYJrDIo3q88gZ2AWmjW+u9QyrwFIAIgYImkO4yTjhOBqojVsnAqvhxjBPQ1uWbycQMsNjvArrg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":263373},"type":"module","_from":"file:a16n-pact-client-1.0.0.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"scripts":{"tc":"tsc --noEmit","test":"vitest run","build":"tsup","check":"eslint . && prettier --check .","clean":"rm -rf dist","format":"eslint --fix . && prettier --write .","test:watch":"vitest"},"_npmUser":{"name":"halcyon400","email":"alex@a16n.dev"},"_resolved":"/private/var/folders/nh/p9l2vgsn4r3gr5__dt4wx7k00000gn/T/e14f563cc44a58e34d03b866707d9a04/a16n-pact-client-1.0.0.tgz","_integrity":"sha512-kVJnzo3y2OTs9DdD5BoP+X9Q0wEabPFAhtUjJbHOLLAG6SAS2eKVKXnbZqwfcC3lvHngm1aYJ4hboklJZlWxMw==","_npmVersion":"10.9.4","description":"Local-first document store with optional sync, realtime, blobs, migrations, and end-to-end encryption — the client half of Pact.","directories":{},"_nodeVersion":"22.22.0","dependencies":{"zod":"4.4.1","dayjs":"^1.11.13","nanoid":"^5.1.7"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8","vitest":"^3","typescript":"~6.0.3"},"_npmOperationalInternal":{"tmp":"tmp/pact-client_1.0.0_1784712682475_0.4857026629418699","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@a16n/pact-client","version":"1.1.0","license":"UNLICENSED","_id":"@a16n/pact-client@1.1.0","maintainers":[{"name":"halcyon400","email":"alex@a16n.dev"}],"dist":{"shasum":"b9a27013903ab86aa5fc45df94011f58840e354d","tarball":"https://registry.npmjs.org/@a16n/pact-client/-/pact-client-1.1.0.tgz","fileCount":5,"integrity":"sha512-6xu6uuKAFf7yCUxdSXxPXBlouoGbbJC13fBey7hl9bFI4lNhPx2sloxyafZ+jkQOuhiERn/PKo1RYm46jwG9Mg==","signatures":[{"sig":"MEYCIQCq78SNM2HtDjnX/niahbRm4ylQOtmOS7/LNlmE353MRgIhAIHimr0DpUt3AvBvilXiXrsYr8HKNIswmWdq+40NXEwG","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":282159},"type":"module","_from":"file:a16n-pact-client-1.1.0.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"scripts":{"tc":"tsc --noEmit","test":"vitest run","build":"tsup","check":"eslint . && prettier --check .","clean":"rm -rf dist","format":"eslint --fix . && prettier --write .","test:watch":"vitest"},"_npmUser":{"name":"halcyon400","email":"alex@a16n.dev"},"_resolved":"/private/var/folders/nh/p9l2vgsn4r3gr5__dt4wx7k00000gn/T/8147227cfff51e9a123a22172b40f5fd/a16n-pact-client-1.1.0.tgz","_integrity":"sha512-6xu6uuKAFf7yCUxdSXxPXBlouoGbbJC13fBey7hl9bFI4lNhPx2sloxyafZ+jkQOuhiERn/PKo1RYm46jwG9Mg==","_npmVersion":"10.9.4","description":"Local-first document store with optional sync, realtime, blobs, migrations, and end-to-end encryption — the client half of Pact.","directories":{},"_nodeVersion":"22.22.0","dependencies":{"zod":"4.4.1","dayjs":"^1.11.13","nanoid":"^5.1.7"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8","vitest":"^3","typescript":"~6.0.3"},"_npmOperationalInternal":{"tmp":"tmp/pact-client_1.1.0_1784785346387_0.39885137402539295","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@a16n/pact-client","version":"1.2.0","description":"Local-first document store with optional sync, realtime, blobs, migrations, and end-to-end encryption — the client half of Pact.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/a16n-dev/pact.git","directory":"packages/client"},"homepage":"https://github.com/a16n-dev/pact#readme","bugs":"https://github.com/a16n-dev/pact/issues","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"dependencies":{"dayjs":"^1.11.13","nanoid":"^5.1.7","zod":"4.4.1"},"devDependencies":{"tsup":"^8","typescript":"~6.0.3","vitest":"^3"},"scripts":{"build":"tsup","clean":"rm -rf dist","test":"vitest run","test:watch":"vitest","tc":"tsc --noEmit -p tsconfig.test.json","check":"eslint . && prettier --check .","format":"eslint --fix . && prettier --write ."},"_nodeVersion":"22.23.1","_id":"@a16n/pact-client@1.2.0","dist":{"integrity":"sha512-THfwV/xpnadxV/7mHC69f8nbqBTpEqnQAehgDlgq7LiGfqOX4LABoqZafcnmHRPEcVYusVmTcJUWfbKJLVFwDg==","shasum":"52dde39000c93c5d155450ed08010b99e25b7a1f","tarball":"https://registry.npmjs.org/@a16n/pact-client/-/pact-client-1.2.0.tgz","fileCount":6,"unpackedSize":344988,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@a16n%2fpact-client@1.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD/QWBozDcjmHa7TNHQkXGBKrfjx2PJOfC3xoAUzRhYtQIgDd+fBu08Lk+n0QWQB6oP4DDkqO1mlYU6OFZ2WrF2vaU="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ceb21a72-1312-4cc3-af11-77330883cfa7"}},"directories":{},"maintainers":[{"name":"halcyon400","email":"alex@a16n.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pact-client_1.2.0_1785999282416_0.8664031397437248"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-22T09:31:22.235Z","modified":"2026-08-06T06:54:42.866Z","1.0.0":"2026-07-22T09:31:22.622Z","1.1.0":"2026-07-23T05:42:26.546Z","1.2.0":"2026-08-06T06:54:42.560Z"},"license":"MIT","description":"Local-first document store with optional sync, realtime, blobs, migrations, and end-to-end encryption — the client half of Pact.","maintainers":[{"name":"halcyon400","email":"alex@a16n.dev"}],"readme":"# Pact\n\nA general purpose client library for document-based data storage. Local-only by default, with sync and realtime capabilities built in.\n\n```\nnpm install @a16n/pact-client\n```\n\nPact deliberately trades generality for simplicity:\n\n- **A small, high-trust group.** One server per app group (e.g. a household). Auth is one shared app password traded for per-client tokens; there's no per-document access control.\n- **Last-write-wins is good enough.** Conflicts resolve by `updatedAt`. No CRDTs, no merge UIs.\n- **Schemas are required, and owned by you.** The collection definitions you hand the store _are_ the set of collections that exist — writes validate against them, and undefined collections are rejected.\n\n## What you definitely need to know\n\n### Creating a store\n\nPact uses a `store` which you can interact with. Create a store like so:\n\n```ts\nimport { Store } from '@a16n/pact-client';\n\nconst databaseAdapter = new InMemoryAdapter();\n\nconst store = await Store.create({\n  adapter: databaseAdapter,\n  collections: [todos],\n});\n```\n\nThe `adapter` lets pact use the storage backend of your choice. Recipes are provided here that you can copy+paste for common storage backends (localStorage, indexedDb, sqlite, file system, ...).\n\nAlternatively implement your own: see `DatabaseAdapter` for a full reference. Everything else a store can take (blobs, encryption, hooks) is on `StoreOptions`.\n\n### Creating collections\n\nNow you'll need some collections, to continue our earlier example:\n\n```ts\nconst todos = defineCollection({\n  name: 'todos',\n  idPrefix: 'td',\n  schema: (base) =>\n    base.extend({\n      title: z.string().min(1),\n      done: z.boolean().default(false),\n    }),\n});\n```\n\nThe schema can be any zod schema extending the base schema. Every document id carries the collection's prefix (`td-Ab3xY9kQz2`) — enforced at runtime _and_ in the type system, so passing a todo id where a recipe id belongs is a compile error. See `CollectionConfig` for the full set of options.\n\n### All the CRUD you'd expect\n\nDocuments are read and written through a collection handle, fully typed from the schema:\n\n```ts\nconst todosCollection = store.collection('todos');\n\n// Create one — the id is generated for you (pass `id` to choose your own)\nconst todo = await todosCollection.create({ title: 'Do laundry' });\n\n// Find one\ntodosCollection.get('td-123'); // returns null if not found\n\n// List all\ntodosCollection.list();\n\n// Update one — a partial merge: fields you omit are left alone\ntodosCollection.update('td-123', { title: 'Do laundry' });\n\n// Create-or-update by id\ntodosCollection.upsert({ id: 'td-123', title: 'Do laundry' });\n\n// Delete one (soft: a tombstone remains, so the delete syncs)\ntodosCollection.delete('td-123');\n```\n\nEach of `create`/`update`/`delete` has a `...Many` batch form, `get`/`list` accept `{ includeDeleted: true }` to see tombstones, and `pull`/`pullAll` fetch fresh from a sync server. See `Collection` for the full reference.\n\n### Reacting to changes\n\nEvery mutation — local writes, pulled changes from other devices, blob activity — emits a `change` event with the collection that changed. Hang your UI invalidation off it:\n\n```ts\nstore.on('change', (collection) => {\n  // re-read whatever your UI shows from that collection\n});\n```\n\nThat's all you strictly need to know. Read on for more features that you'll probably need\n\n## What you might need to know\n\n### Migrations\n\nSooner or later you'll want to change a schema. Pact supports migrations, which do exactly this.\n\nWe actually wanted `done` to be called `completed`, so we can add a migration to rename the field:\n\n```ts\nconst todos = defineCollection({\n  name: 'todos',\n  idPrefix: 'td',\n  schema: (base) =>\n    base.extend({\n      title: z.string().min(1),\n      completed: z.boolean().default(false),\n    }),\n  migrations: {\n    current: 2,\n    migrations: [\n      {\n        from: 1,\n        to: 2,\n        up: (doc) => {\n          doc.completed = doc.done;\n          return doc;\n        },\n      },\n    ],\n  },\n});\n```\n\nOld documents upgrade lazily as they're read. Note that you don't always need a migration. If you add a new field with a default value, they will still parse correctly\n\n### Syncing\n\nPoint the store at a pact server and the same CRUD code syncs, with offline writes queued durably:\n\n```ts\nawait store.sync.register(url, appPassword, 'myapp', \"Alice's laptop\"); // once per install\nawait store.author.set('us-alice'); // claim who this device writes as\nawait store.author.reassignLocal('us-alice'); // adopt any pre-identity writes\n\nawait store.sync.push(); // drain queued writes, push everything\nawait todosCollection.pullAll(); // pull everyone else's changes\n```\n\nRegistration persists, so future launches reconnect automatically — and when the server advertises realtime, changes from other devices arrive over a WebSocket with no extra code. Everything server-related lives under `store.sync`; identity under `store.author`.\n\n### Binary blobs\n\nIf your app is like most apps, it probably needs to store binary data - images, video, pdfs etc. Blobs are content-addressed: the key is the SHA-256 of the bytes, so writes are idempotent and dedupe is automatic. Pass a `BlobAdapter` as `blobs` when creating the store, and declare a `blobHashes` extractor so pact knows which blobs your documents reference:\n\n```ts\nconst store = await Store.create({\n  adapter: databaseAdapter,\n  blobs: myBlobAdapter, // a BlobAdapter — recipes provided, or implement the 7-method interface\n  collections: [photos],\n  blobHashes: blobFields({ photos: ['imageHash'] }), // which fields hold blob references\n});\n\n// Write bytes, reference them from a document by hash\nconst hash = await store.blobs.write(jpegBytes, 'image/jpeg');\nawait store.collection('photos').create({ caption: 'Sunset', imageHash: hash });\n\n// Render it\nconst uri = store.blobs.uri(hash); // e.g. file://… — null if not local yet\n\n// Syncing (with a registered server)\nawait store.blobs.push(); // upload blobs the server doesn't have\nawait store.blobs.pullReferenced(); // download blobs your docs reference but you don't hold\nawait store.blobs.prune(); // locally delete blobs no live doc references\n```\n\nThe `blobHashes` extractor is what makes `pullReferenced` and `prune` possible — without it pact can't tell a referenced blob from an orphan. `blobFields` covers flat fields; write the function by hand for nested references.\n\n## What you probably don't need to know\n\n### Backups\n\nPact can pack every document (and optionally blobs) into a single portable archive, independent of any server:\n\n```ts\nconst bytes = await store.backup.create(); // persist however you like\nawait store.backup.restore(bytes); // merge (last-write-wins)\nawait store.backup.restore(bytes, { mode: 'replace' });\n```\n\n### End-to-end encryption\n\nPass `encryption: { cipher }` when creating the store and domain fields are sealed into ciphertext — at rest locally and on the sync wire; the server only ever sees base sync fields (ids, timestamps, authors) plus the envelope:\n\n```ts\nconst key = await deriveEncryptionKey(passphrase, 'myapp');\nconst store = await Store.create({\n  adapter: databaseAdapter,\n  collections: [todos],\n  encryption: { cipher: createWebCryptoCipher(key) },\n});\n```\n\nWrong keys fail fast at startup. `createWebCryptoCipher` covers Node/web/Workers; React Native apps inject their own two-method `DocCipher`. All clients of the app must hold the same key — losing it loses the server-side data. Key management lives under `store.encryption`.\n\n### Seeds\n\n`store.seed()` loads versioned reference data identically on every client without syncing it — system-authored docs that user edits always win over.\n\n### Indexes\n\nMost of the time, calling `collection.list()` and filtering after the fact is good enough. If you really need to, you can declare indexes on a collection with `.withIndexes()` and then query by index value:\n\n```ts\nconst todos = defineCollection({\n  name: 'todos',\n  idPrefix: 'td',\n  schema: (base) =>\n    base.extend({\n      title: z.string().min(1),\n      done: z.boolean().default(false),\n      tags: z.array(z.string()).default([]),\n    }),\n}).withIndexes({\n  done: (doc) => doc.done, // doc is fully typed here\n  tags: (doc) => doc.tags, // multi-valued: indexed under each tag\n});\n\n// Then query by index — index name and value are both checked at compile time\nconst doneTodos = await store.collection('todos').listByIndex('done', true);\nconst urgentTodos = await store.collection('todos').listByIndex('tags', 'urgent');\n```\n\nAn extractor returns the key(s) a doc is found under — one value, an array (each indexed\nseparately), or `[]` to exclude it. Indexes are local, in-memory derived state: computed\nfrom the decrypted, migrated docs, rebuilt on every `Store.create`, and never persisted or\nsynced. Equality/membership only — no ranges or sorting. `listByIndex` resolves matches\nthrough `getMany`, so results are always migrated and tombstone-free even if the index lags.\n\nAgain, you probably don't need this.\n\n## License\n\nUNLICENSED — published for the author's own projects; no rights granted for other use.\n","readmeFilename":"","homepage":"https://github.com/a16n-dev/pact#readme","repository":{"type":"git","url":"git+https://github.com/a16n-dev/pact.git","directory":"packages/client"},"bugs":"https://github.com/a16n-dev/pact/issues"}