{"_id":"@10xmedia/payload-carddav-sync","_rev":"4-10230cf8d1c20a1828607091c3d4bba4","name":"@10xmedia/payload-carddav-sync","dist-tags":{"latest":"0.0.4"},"versions":{"0.0.1":{"name":"@10xmedia/payload-carddav-sync","version":"0.0.1","license":"MIT","_id":"@10xmedia/payload-carddav-sync@0.0.1","maintainers":[{"name":"harleysalas","email":"Harley@Harleysalas.com"},{"name":"10xmedia-org","email":"info@10xmedia.de"},{"name":"veiagg","email":"romapalamar.veiag@gmail.com"}],"dist":{"shasum":"c2d578d4d63e9eab65e5a0226e7affdcd3bf7e15","tarball":"https://registry.npmjs.org/@10xmedia/payload-carddav-sync/-/payload-carddav-sync-0.0.1.tgz","fileCount":38,"integrity":"sha512-+vRP6DSCeRcSg/kZx2e+jBJLeILhkXYakeZ/6NwxDFTkTdcHCyXziyuc8WvY6HTyqiTD8BPMiRvK0SifNztGug==","signatures":[{"sig":"MEUCIELl/KPQt3hKyVuMhoL+1ehE1PtqTlA+Q3zS1F6ZfBvfAiEAk5RuhuQ2D6hQmEFXhqaehn0hM9BoeV9JPoUmbIa+r34=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":125520},"main":"./dist/index.js","type":"module","_from":"file:10xmedia-payload-carddav-sync-0.0.1.tgz","types":"./dist/index.d.ts","engines":{"node":"^18.20.2 || >=20.9.0","pnpm":"^9 || ^10"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./rsc":{"types":"./dist/exports/rsc.d.ts","import":"./dist/exports/rsc.js","default":"./dist/exports/rsc.js"},"./client":{"types":"./dist/exports/client.d.ts","import":"./dist/exports/client.js","default":"./dist/exports/client.js"}},"scripts":{"dev":"next dev dev --turbo","lint":"eslint","test":"pnpm test:int && pnpm test:e2e","build":"pnpm copyfiles && pnpm build:types && pnpm build:swc","clean":"rimraf {dist,*.tsbuildinfo}","lint:fix":"eslint ./src --fix","test:e2e":"playwright test","test:int":"vitest","build:swc":"swc ./src -d ./dist --config-file .swcrc --strip-leading-paths","copyfiles":"copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/","build:types":"tsc --outDir dist --rootDir ./src","dev:payload":"cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload","generate:types":"pnpm dev:generate-types","dev:generate-types":"pnpm dev:payload generate:types","generate:importmap":"pnpm dev:generate-importmap","dev:generate-importmap":"pnpm dev:payload generate:importmap"},"_npmUser":{"name":"veiagg","email":"romapalamar.veiag@gmail.com"},"registry":"https://registry.npmjs.org/","_resolved":"/private/var/folders/sh/ly8rsxlj2ddd3sv3lh9c8w200000gn/T/0d3ea7818e79d33882136a858bb52521/10xmedia-payload-carddav-sync-0.0.1.tgz","_integrity":"sha512-+vRP6DSCeRcSg/kZx2e+jBJLeILhkXYakeZ/6NwxDFTkTdcHCyXziyuc8WvY6HTyqiTD8BPMiRvK0SifNztGug==","_npmVersion":"10.9.2","description":"Payload CMS plugin for bidirectional CardDAV synchronization","directories":{},"_nodeVersion":"22.16.0","dependencies":{"tsdav":"^2.1.8"},"_hasShrinkwrap":false,"devDependencies":{"next":"16.2.3","open":"^10.1.0","react":"19.2.4","sharp":"0.34.2","eslint":"^9.23.0","qs-esm":"8.0.1","rimraf":"3.0.2","vitest":"4.0.18","graphql":"^16.8.1","payload":"3.82.1","@swc/cli":"0.6.0","prettier":"^3.4.2","copyfiles":"2.4.1","cross-env":"^7.0.3","react-dom":"19.2.4","typescript":"5.7.3","@types/node":"22.19.9","@types/react":"19.2.14","@payloadcms/ui":"3.82.1","@eslint/eslintrc":"^3.2.0","@payloadcms/next":"3.82.1","@playwright/test":"1.58.2","@types/react-dom":"19.2.3","sort-package-json":"^2.10.0","@swc-node/register":"1.10.9","eslint-config-next":"16.2.3","vite-tsconfig-paths":"6.0.5","@payloadcms/db-sqlite":"3.82.1","mongodb-memory-server":"10.1.4","@payloadcms/db-mongodb":"3.82.1","@payloadcms/db-postgres":"3.82.1","@payloadcms/eslint-config":"3.28.0","@payloadcms/richtext-lexical":"3.82.1"},"peerDependencies":{"payload":"^3.82.1"},"_npmOperationalInternal":{"tmp":"tmp/payload-carddav-sync_0.0.1_1777636208111_0.7072604840617496","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@10xmedia/payload-carddav-sync","version":"0.0.2","license":"MIT","_id":"@10xmedia/payload-carddav-sync@0.0.2","maintainers":[{"name":"harleysalas","email":"Harley@Harleysalas.com"},{"name":"10xmedia-org","email":"info@10xmedia.de"},{"name":"veiagg","email":"romapalamar.veiag@gmail.com"}],"dist":{"shasum":"6e3bcbf37c50dd871439e4b7ce175c3195a418b8","tarball":"https://registry.npmjs.org/@10xmedia/payload-carddav-sync/-/payload-carddav-sync-0.0.2.tgz","fileCount":38,"integrity":"sha512-m/AShNebgEziamNX+L0XESrbULGbUMRGTclZ45JeYXPWSgIK2mjZsg9tdMDX9I7Q+p7oBhta5ReQjtm+oN0yrg==","signatures":[{"sig":"MEUCIAfnhkM9jQ2eEwTEzPQtDk34zEwsPbvpmJyZ5nryEaOIAiEA9a1r2xhP2vC+StXJnQQXOenXGG9Dy8Pw3a8k1QJbRNk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":157353},"main":"./dist/index.js","type":"module","_from":"file:10xmedia-payload-carddav-sync-0.0.2.tgz","types":"./dist/index.d.ts","engines":{"node":"^18.20.2 || >=20.9.0","pnpm":"^9 || ^10"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./rsc":{"types":"./dist/exports/rsc.d.ts","import":"./dist/exports/rsc.js","default":"./dist/exports/rsc.js"},"./client":{"types":"./dist/exports/client.d.ts","import":"./dist/exports/client.js","default":"./dist/exports/client.js"}},"scripts":{"dev":"next dev dev --turbo","lint":"eslint","test":"pnpm test:int && pnpm test:e2e","build":"pnpm copyfiles && pnpm build:types && pnpm build:swc","clean":"rimraf {dist,*.tsbuildinfo}","lint:fix":"eslint ./src --fix","test:e2e":"playwright test","test:int":"vitest","build:swc":"swc ./src -d ./dist --config-file .swcrc --strip-leading-paths","copyfiles":"copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/","build:types":"tsc --outDir dist --rootDir ./src","dev:payload":"cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload","generate:types":"pnpm dev:generate-types","dev:generate-types":"pnpm dev:payload generate:types","generate:importmap":"pnpm dev:generate-importmap","dev:generate-importmap":"pnpm dev:payload generate:importmap"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:3fa8f2ac-6aa5-4be7-afe6-34949cd607a4"}},"registry":"https://registry.npmjs.org/","_resolved":"/tmp/23258f0ccead8405ecbe3ef025c8e301/10xmedia-payload-carddav-sync-0.0.2.tgz","_integrity":"sha512-m/AShNebgEziamNX+L0XESrbULGbUMRGTclZ45JeYXPWSgIK2mjZsg9tdMDX9I7Q+p7oBhta5ReQjtm+oN0yrg==","_npmVersion":"11.13.0","description":"Payload CMS plugin for bidirectional CardDAV synchronization","directories":{},"_nodeVersion":"20.20.2","dependencies":{"tsdav":"^2.1.8"},"_hasShrinkwrap":false,"devDependencies":{"next":"16.2.3","open":"^10.1.0","react":"19.2.4","sharp":"0.34.2","eslint":"^9.23.0","qs-esm":"8.0.1","rimraf":"3.0.2","vitest":"4.0.18","graphql":"^16.8.1","payload":"3.82.1","@swc/cli":"0.6.0","prettier":"^3.4.2","copyfiles":"2.4.1","cross-env":"^7.0.3","react-dom":"19.2.4","typescript":"5.7.3","@types/node":"22.19.9","@types/react":"19.2.14","@payloadcms/ui":"3.82.1","@eslint/eslintrc":"^3.2.0","@payloadcms/next":"3.82.1","@playwright/test":"1.58.2","@types/react-dom":"19.2.3","sort-package-json":"^2.10.0","@swc-node/register":"1.10.9","eslint-config-next":"16.2.3","vite-tsconfig-paths":"6.0.5","@payloadcms/db-sqlite":"3.82.1","mongodb-memory-server":"10.1.4","@payloadcms/db-mongodb":"3.82.1","@payloadcms/db-postgres":"3.82.1","@payloadcms/eslint-config":"3.28.0","@payloadcms/richtext-lexical":"3.82.1"},"peerDependencies":{"payload":"^3.82.1"},"_npmOperationalInternal":{"tmp":"tmp/payload-carddav-sync_0.0.2_1778073852932_0.5198211398553467","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@10xmedia/payload-carddav-sync","version":"0.0.3","license":"MIT","_id":"@10xmedia/payload-carddav-sync@0.0.3","maintainers":[{"name":"harleysalas","email":"Harley@Harleysalas.com"},{"name":"10xmedia-org","email":"info@10xmedia.de"},{"name":"veiagg","email":"romapalamar.veiag@gmail.com"}],"dist":{"shasum":"8f382c898dcf77db13d5e259acf6d872b270d4b9","tarball":"https://registry.npmjs.org/@10xmedia/payload-carddav-sync/-/payload-carddav-sync-0.0.3.tgz","fileCount":38,"integrity":"sha512-eh6a5HPlnHpAqiUnjQFGo8rfYa0LaM5j3n4S4j/m0+uf1qSeW21JjdQVJ2zENrAe+WnQ9WfS6Kde8AEgijVDMA==","signatures":[{"sig":"MEYCIQC5NdPIfeXlqAz4kUi3zjNI6RNTvgizR25mwKfS9ztcQAIhAPm6XJ/oBDCFxo47V+P7H3OyN8Skjinli20i6RcewsCc","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":163699},"main":"./dist/index.js","type":"module","_from":"file:10xmedia-payload-carddav-sync-0.0.3.tgz","types":"./dist/index.d.ts","engines":{"node":"^18.20.2 || >=20.9.0","pnpm":"^9 || ^10"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./rsc":{"types":"./dist/exports/rsc.d.ts","import":"./dist/exports/rsc.js","default":"./dist/exports/rsc.js"},"./client":{"types":"./dist/exports/client.d.ts","import":"./dist/exports/client.js","default":"./dist/exports/client.js"}},"scripts":{"dev":"next dev dev --turbo","lint":"eslint","test":"pnpm test:int && pnpm test:e2e","build":"pnpm copyfiles && pnpm build:types && pnpm build:swc","clean":"rimraf {dist,*.tsbuildinfo}","lint:fix":"eslint ./src --fix","test:e2e":"playwright test","test:int":"vitest","build:swc":"swc ./src -d ./dist --config-file .swcrc --strip-leading-paths","copyfiles":"copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/","build:types":"tsc --outDir dist --rootDir ./src","dev:payload":"cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload","generate:types":"pnpm dev:generate-types","dev:generate-types":"pnpm dev:payload generate:types","generate:importmap":"pnpm dev:generate-importmap","dev:generate-importmap":"pnpm dev:payload generate:importmap"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:3fa8f2ac-6aa5-4be7-afe6-34949cd607a4"}},"registry":"https://registry.npmjs.org/","_resolved":"/tmp/5f34658179ebd8cdc2b8ef270a5d615b/10xmedia-payload-carddav-sync-0.0.3.tgz","_integrity":"sha512-eh6a5HPlnHpAqiUnjQFGo8rfYa0LaM5j3n4S4j/m0+uf1qSeW21JjdQVJ2zENrAe+WnQ9WfS6Kde8AEgijVDMA==","_npmVersion":"11.13.0","description":"Payload CMS plugin for bidirectional CardDAV synchronization","directories":{},"_nodeVersion":"20.20.2","dependencies":{"tsdav":"^2.1.8"},"_hasShrinkwrap":false,"devDependencies":{"next":"16.2.3","open":"^10.1.0","react":"19.2.4","sharp":"0.34.2","eslint":"^9.23.0","qs-esm":"8.0.1","rimraf":"3.0.2","vitest":"4.0.18","graphql":"^16.8.1","payload":"3.82.1","@swc/cli":"0.6.0","prettier":"^3.4.2","copyfiles":"2.4.1","cross-env":"^7.0.3","react-dom":"19.2.4","typescript":"5.7.3","@types/node":"22.19.9","@types/react":"19.2.14","@payloadcms/ui":"3.82.1","@eslint/eslintrc":"^3.2.0","@payloadcms/next":"3.82.1","@playwright/test":"1.58.2","@types/react-dom":"19.2.3","sort-package-json":"^2.10.0","@swc-node/register":"1.10.9","eslint-config-next":"16.2.3","vite-tsconfig-paths":"6.0.5","@payloadcms/db-sqlite":"3.82.1","mongodb-memory-server":"10.1.4","@payloadcms/db-mongodb":"3.82.1","@payloadcms/db-postgres":"3.82.1","@payloadcms/eslint-config":"3.28.0","@payloadcms/richtext-lexical":"3.82.1"},"peerDependencies":{"payload":"^3.82.1"},"_npmOperationalInternal":{"tmp":"tmp/payload-carddav-sync_0.0.3_1778077500445_0.3690647721405229","host":"s3://npm-registry-packages-npm-production"}},"0.0.4":{"name":"@10xmedia/payload-carddav-sync","version":"0.0.4","description":"Payload CMS plugin for bidirectional CardDAV synchronization","license":"MIT","type":"module","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts","default":"./dist/index.js"},"./client":{"import":"./dist/exports/client.js","types":"./dist/exports/client.d.ts","default":"./dist/exports/client.js"},"./rsc":{"import":"./dist/exports/rsc.js","types":"./dist/exports/rsc.d.ts","default":"./dist/exports/rsc.js"}},"main":"./dist/index.js","types":"./dist/index.d.ts","devDependencies":{"@eslint/eslintrc":"^3.2.0","@payloadcms/db-mongodb":"3.82.1","@payloadcms/db-postgres":"3.82.1","@payloadcms/db-sqlite":"3.82.1","@payloadcms/eslint-config":"3.28.0","@payloadcms/next":"3.82.1","@payloadcms/richtext-lexical":"3.82.1","@payloadcms/ui":"3.82.1","@playwright/test":"1.58.2","@swc-node/register":"1.10.9","@swc/cli":"0.6.0","@types/node":"22.19.9","@types/react":"19.2.14","@types/react-dom":"19.2.3","copyfiles":"2.4.1","cross-env":"^7.0.3","eslint":"^9.23.0","eslint-config-next":"16.2.3","graphql":"^16.8.1","mongodb-memory-server":"10.1.4","next":"16.2.3","open":"^10.1.0","payload":"3.82.1","prettier":"^3.4.2","qs-esm":"8.0.1","react":"19.2.4","react-dom":"19.2.4","rimraf":"3.0.2","sharp":"0.34.2","sort-package-json":"^2.10.0","typescript":"5.7.3","vite-tsconfig-paths":"6.0.5","vitest":"4.0.18"},"peerDependencies":{"payload":"^3.82.1"},"engines":{"node":"^18.20.2 || >=20.9.0","pnpm":"^9 || ^10"},"registry":"https://registry.npmjs.org/","dependencies":{"tsdav":"^2.1.8"},"scripts":{"build":"pnpm copyfiles && pnpm build:types && pnpm build:swc","build:swc":"swc ./src -d ./dist --config-file .swcrc --strip-leading-paths","build:types":"tsc --outDir dist --rootDir ./src","clean":"rimraf {dist,*.tsbuildinfo}","copyfiles":"copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/","dev":"next dev dev --turbo","dev:generate-importmap":"pnpm dev:payload generate:importmap","dev:generate-types":"pnpm dev:payload generate:types","dev:payload":"cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload","generate:importmap":"pnpm dev:generate-importmap","generate:types":"pnpm dev:generate-types","lint":"eslint","lint:fix":"eslint ./src --fix","test":"pnpm test:int && pnpm test:e2e","test:e2e":"playwright test","test:int":"vitest"},"_id":"@10xmedia/payload-carddav-sync@0.0.4","_integrity":"sha512-dgpAVdOqYRR5ypkwgTtB2NxwhfJY3mjyMjlfzeDp85PCyujbFwJLppMJk6rM2g0s03UdLB7C8n4raEqybb2kUA==","_resolved":"/tmp/fa6bb069ed8915872f4060caa3aa8516/10xmedia-payload-carddav-sync-0.0.4.tgz","_from":"file:10xmedia-payload-carddav-sync-0.0.4.tgz","_nodeVersion":"20.20.2","_npmVersion":"11.13.0","dist":{"integrity":"sha512-dgpAVdOqYRR5ypkwgTtB2NxwhfJY3mjyMjlfzeDp85PCyujbFwJLppMJk6rM2g0s03UdLB7C8n4raEqybb2kUA==","shasum":"752a4ba915e6f9f3a8cf878e499448353f9b21c8","tarball":"https://registry.npmjs.org/@10xmedia/payload-carddav-sync/-/payload-carddav-sync-0.0.4.tgz","fileCount":38,"unpackedSize":165427,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD4op6jnp8kcCC78gF7ZLKMPzgm2Fkv7NK6vSh9jhSH2QIhANOTaQeYKked+rn6/BcqEir+bKiutyLbYpzJQvMfVLDx"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:3fa8f2ac-6aa5-4be7-afe6-34949cd607a4"}},"directories":{},"maintainers":[{"name":"harleysalas","email":"Harley@Harleysalas.com"},{"name":"10xmedia-org","email":"info@10xmedia.de"},{"name":"veiagg","email":"romapalamar.veiag@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/payload-carddav-sync_0.0.4_1778087018339_0.5161303221888625"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-01T11:50:08.053Z","modified":"2026-05-06T17:03:38.670Z","0.0.1":"2026-05-01T11:50:08.280Z","0.0.2":"2026-05-06T13:24:13.086Z","0.0.3":"2026-05-06T14:25:00.582Z","0.0.4":"2026-05-06T17:03:38.519Z"},"license":"MIT","description":"Payload CMS plugin for bidirectional CardDAV synchronization","maintainers":[{"name":"harleysalas","email":"Harley@Harleysalas.com"},{"name":"10xmedia-org","email":"info@10xmedia.de"},{"name":"veiagg","email":"romapalamar.veiag@gmail.com"}],"readme":"![Payload CardDAV Sync Banner](./assets/banner.jpg)\n\nA [Payload CMS](https://payloadcms.com) plugin for bidirectional CardDAV synchronization. Keeps a Payload collection in sync with a CardDAV address book (Nextcloud, Mailcow/SOGo, iCloud, Baikal, etc.).\n\n## How it works\n\n- **Payload → CardDAV**: every time a document is created, updated, or deleted in the configured collection, the change is pushed to CardDAV immediately via hooks.\n- **CardDAV → Payload**: a Payload Jobs task runs on a cron schedule, fetches the address book, and updates Payload for any contacts whose ETag changed.\n\nChanges made in Payload win immediately. Changes made in a CardDAV client (phone, Thunderbird, etc.) are pulled in on the next scheduled sync.\n\n> **Important:** the scheduled sync only propagates changes **from CardDAV into Payload**. On the first job run, all CardDAV contacts are imported into Payload automatically. Documents that exist in Payload but have never been pushed to CardDAV (e.g. created via a script that bypassed the plugin hooks) will **not** be synced to CardDAV automatically — only creates and updates flowing through the `afterChange` hook push to CardDAV. To get existing Payload documents into CardDAV, edit and save each one to trigger the hook.\n\n## Installation\n\n```sh\npnpm add @10xmedia/payload-carddav-sync\n# or\nnpm install @10xmedia/payload-carddav-sync\n```\n\n## Quick setup\n\n### 1. Add the plugin to your config\n\n```ts\nimport type { Contact } from './payload-types'\nimport { payloadCarddavSync } from '@10xmedia/payload-carddav-sync'\n\nexport default buildConfig({\n  plugins: [\n    payloadCarddavSync<Contact>({\n      auth: {\n        serverUrl: process.env.CARDDAV_SERVER_URL!,\n        addressbookPath: process.env.CARDDAV_ADDRESSBOOK_PATH!,\n        username: process.env.CARDDAV_USERNAME!,\n        password: process.env.CARDDAV_PASSWORD!,\n      },\n      collection: 'contacts',\n      fieldMapping: {\n        fromVCard: (vcard, req) => ({\n          firstName: vcard.fn?.value?.split(' ')[0],\n          lastName:  vcard.fn?.value?.split(' ').slice(1).join(' ') || undefined,\n          email:     vcard.email?.[0]?.value,\n          phone:     vcard.tel?.[0]?.value,\n        }),\n        toVCard: (doc, req) => ({\n          fn:    { value: [doc.firstName, doc.lastName].filter(Boolean).join(' ') },\n          email: doc.email ? [{ value: doc.email, type: 'home' }] : undefined,\n          tel:   doc.phone ? [{ value: doc.phone, type: 'cell' }] : undefined,\n        }),\n      },\n      sync: {\n        cron: '*/5 * * * *',\n        queue: 'carddav-sync',\n      },\n    }),\n  ],\n})\n```\n\n### 2. Configure the job runner\n\nThe plugin registers a Payload Jobs task but does not configure the runner — this is your responsibility since it depends on your deployment.\n\n**Dedicated server — use `autoRun`:**\n\n```ts\nexport default buildConfig({\n  jobs: {\n    autoRun: [\n      { cron: '* * * * *', queue: 'carddav-sync' },\n    ],\n  },\n  plugins: [/* ... */],\n})\n```\n\n**Serverless — trigger via API:**\n\nSet up a cron (Vercel Cron, etc.) to call:\n```\nGET /api/payload-jobs/run?queue=carddav-sync\n```\n\n## Configuration reference\n\n```ts\npayloadCarddavSync({\n  // Required\n  auth: CardDavAuth\n  collection: CollectionSlug\n  fieldMapping: {\n    fromVCard: (vcard: VCardProperties, req: PayloadRequest) => Partial<TDoc> | Promise<Partial<TDoc>>\n    toVCard: (doc: TDoc, req: PayloadRequest) => VCardProperties | Promise<VCardProperties>\n  }\n\n  // Optional\n  debug?: boolean          // show cardDavSync fields in admin UI. Default: false\n  disabled?: boolean       // disable sync but keep schema fields intact\n  mergeStrategy?: 'replace' | 'merge'  // how Payload writes to CardDAV. Default: 'merge'\n  sync?: {\n    cron?: string          // schedule for the sync job. Default: '*/5 * * * *'\n    queue?: string         // Payload job queue name. Default: 'carddav-sync'\n    concurrency?: number   // parallel Payload operations per batch. Default: 10\n    maxRetries?: number    // retry attempts on transient errors. Default: 3\n  }\n})\n```\n\n### Auth\n\n**Basic / Digest:**\n\n```ts\nauth: {\n  serverUrl: 'https://nextcloud.example.com',\n  addressbookPath: '/remote.php/dav/addressbooks/users/admin/contacts/',\n  username: 'admin',\n  password: 'secret',\n  authMethod: 'Basic', // or 'Digest'. Default: 'Basic'\n}\n```\n\n**Bearer token:**\n\n```ts\nauth: {\n  serverUrl: 'https://example.com',\n  addressbookPath: '/dav/addressbooks/user/default/',\n  token: 'my-bearer-token',\n}\n```\n\n### Common server paths\n\n| Server | `addressbookPath` |\n|---|---|\n| Nextcloud | `/remote.php/dav/addressbooks/users/{username}/contacts/` |\n| Mailcow / SOGo | `/SOGo/dav/{email}/Contacts/personal/` |\n| Baikal | `/dav.php/addressbooks/{username}/default/` |\n| iCloud | `/3.0/{dsid}/carddavhome/card/` |\n\n## Field mapping\n\n`fromVCard` maps parsed vCard properties to your Payload doc fields.  \n`toVCard` maps your Payload doc back to vCard properties for writing to CardDAV.\n\nBoth functions receive a `req: PayloadRequest` as their second argument and can be `async`. This lets you look up relationships, fetch related documents, or run any async logic before mapping.\n\n> **Note: by default, the plugin uses `mergeStrategy: 'merge'`** — before writing to CardDAV, it fetches the existing contact and overlays only the fields returned by `toVCard`. Fields absent from `toVCard` are preserved on the server. This protects against accidental data loss.\n>\n> If you set `mergeStrategy: 'replace'`, `toVCard` fully rebuilds the vCard and any field not returned is **permanently deleted** from the contact on CardDAV. Use this only when your mapping covers all fields you care about. See [`mergeStrategy`](#mergestrategy) for details.\n\n### `mergeStrategy`\n\nControls how Payload writes back to CardDAV when a document changes. Default: `'merge'`.\n\n| Value | Behaviour |\n|---|---|\n| `'replace'` | The vCard is fully rebuilt from `toVCard(doc)`. Fields not returned are deleted. |\n| `'merge'` | The existing contact is fetched from CardDAV first. `toVCard(doc)` is overlaid on top — present fields overwrite, absent fields are preserved. |\n\n```ts\npayloadCarddavSync({\n  mergeStrategy: 'replace', // opt out of merge if your mapping covers all fields\n  // ...\n})\n```\n\n**Merge rules:**\n\n- A field returned with a value from `toVCard` → overwrites the CardDAV value.\n- A field returned as `undefined` from `toVCard` → removed from the contact.\n- A field not returned from `toVCard` at all → kept from the existing CardDAV contact.\n\n**Caveats:** merge adds an extra GET request per document update. If the contact no longer exists on CardDAV (deleted remotely), the plugin falls back to replace behavior for that update.\n\n### VCardEntry\n\nAll vCard properties are represented as `VCardEntry` objects to preserve `TYPE` parameters (e.g. `home`, `work`, `cell`):\n\n```ts\ntype VCardEntry = {\n  value: string\n  type?: string  // e.g. 'home', 'work', 'cell', 'internet'\n}\n```\n\nMulti-value fields — `email`, `impp`, `tel`, `url`, `nickname` — are always `VCardEntry[]`, even when only one value is present. Scalar fields — `fn`, `note`, `org`, etc. — are `VCardEntry`. Three fields have dedicated structured types described below.\n\n### VCardNameEntry — the `n` field\n\nThe structured name field `N` (RFC 6350 §6.2.2) is parsed into named components:\n\n```ts\ntype VCardNameEntry = {\n  familyName?: string      // \"Doe\"\n  givenName?: string       // \"John\"\n  additionalNames?: string // middle name\n  honorificPrefix?: string // \"Dr\"\n  honorificSuffix?: string // \"Jr\"\n}\n```\n\n```ts\nfromVCard: (vcard) => ({\n  firstName: vcard.n?.givenName,\n  lastName:  vcard.n?.familyName,\n}),\ntoVCard: (doc) => ({\n  fn: { value: [doc.firstName, doc.lastName].filter(Boolean).join(' ') },\n  n:  { familyName: doc.lastName, givenName: doc.firstName },\n}),\n```\n\n### VCardAdrEntry — the `adr` field\n\nThe address field `ADR` is a structured, repeatable field. The parser returns `VCardAdrEntry[]` — one entry per address:\n\n```ts\ntype VCardAdrEntry = {\n  poBox?: string\n  extendedAddress?: string\n  street?: string\n  city?: string\n  region?: string\n  postalCode?: string\n  country?: string\n  type?: string  // e.g. 'home', 'work'\n}\n```\n\n```ts\nfromVCard: (vcard) => ({\n  street:  vcard.adr?.[0]?.street,\n  city:    vcard.adr?.[0]?.city,\n  country: vcard.adr?.[0]?.country,\n}),\ntoVCard: (doc) => ({\n  adr: doc.street ? [{ type: 'home', street: doc.street, city: doc.city, country: doc.country }] : undefined,\n}),\n```\n\n### `categories` field\n\n`CATEGORIES` is comma-separated and parsed into a plain `string[]`:\n\n```ts\nfromVCard: (vcard) => ({\n  tags: vcard.categories ?? [],\n}),\ntoVCard: (doc) => ({\n  categories: doc.tags,\n}),\n```\n\n### `impp` field\n\n`IMPP` (instant messaging) is a repeatable field, returned as `VCardEntry[]`. The `value` contains the full URI (`xmpp:john@example.com`, `telegram:+380...`), `type` carries the protocol when the client sends one:\n\n```ts\nfromVCard: (vcard) => ({\n  telegram: vcard.impp?.find(e => e.type === 'telegram')?.value?.replace('telegram:', ''),\n}),\ntoVCard: (doc) => ({\n  impp: doc.telegram ? [{ type: 'telegram', value: `telegram:${doc.telegram}` }] : undefined,\n}),\n```\n\n### General field mapping examples\n\n```ts\nfieldMapping: {\n  fromVCard: (vcard, req) => ({\n    // Scalar fields: access .value\n    name:  vcard.fn?.value,\n    org:   vcard.org?.value,\n    note:  vcard.note?.value,\n\n    // Structured name\n    firstName: vcard.n?.givenName,\n    lastName:  vcard.n?.familyName,\n\n    // Multi-value VCardEntry[] fields: pick by index or type\n    email:      vcard.email?.[0]?.value,\n    phone:      vcard.tel?.[0]?.value,\n    homePhone:  vcard.tel?.find(e => e.type === 'home')?.value,\n    workEmail:  vcard.email?.find(e => e.type === 'work')?.value,\n\n    // Structured address\n    street:  vcard.adr?.[0]?.street,\n    city:    vcard.adr?.[0]?.city,\n    country: vcard.adr?.[0]?.country,\n\n    // Categories\n    tags: vcard.categories ?? [],\n  }),\n\n  toVCard: (doc, req) => ({\n    fn:   { value: [doc.firstName, doc.lastName].filter(Boolean).join(' ') },\n    n:    { familyName: doc.lastName, givenName: doc.firstName },\n    org:  doc.org  ? { value: doc.org }  : undefined,\n    note: doc.note ? { value: doc.note } : undefined,\n\n    email: doc.email ? [{ value: doc.email, type: 'home' }] : undefined,\n    tel: [\n      doc.mobilePhone ? { value: doc.mobilePhone, type: 'cell' } : undefined,\n      doc.homePhone   ? { value: doc.homePhone,   type: 'home' } : undefined,\n    ].filter(Boolean) as VCardEntry[],\n\n    adr: doc.street\n      ? [{ type: 'home', street: doc.street, city: doc.city, country: doc.country }]\n      : undefined,\n\n    categories: doc.tags?.length ? doc.tags : undefined,\n  }),\n}\n```\n\n### Async mapping and relationship lookups\n\nBoth functions can be `async`. The `req` parameter gives you a full `PayloadRequest`, so you can fetch related documents or run any server-side logic:\n\n```ts\nfieldMapping: {\n  // Map an org relationship ID → company name for the CardDAV org field\n  toVCard: async (doc, req) => {\n    let orgName: string | undefined\n\n    if (doc.org) {\n      const orgId = typeof doc.org === 'object' ? doc.org.id : doc.org\n      const org = await req.payload.findByID({\n        collection: 'organisations',\n        id: orgId,\n        overrideAccess: true,\n      })\n      orgName = org?.name\n    }\n\n    return {\n      fn:  { value: [doc.firstName, doc.lastName].filter(Boolean).join(' ') },\n      org: orgName ? { value: orgName } : undefined,\n    }\n  },\n\n  // Map the vCard org name → a relationship ID by looking up the collection\n  fromVCard: async (vcard, req) => {\n    let orgId: string | undefined\n\n    if (vcard.org?.value) {\n      const result = await req.payload.find({\n        collection: 'organisations',\n        limit: 1,\n        overrideAccess: true,\n        where: { name: { equals: vcard.org.value } },\n      })\n      orgId = result.docs[0]?.id\n    }\n\n    return {\n      firstName: vcard.fn?.value?.split(' ')[0],\n      lastName:  vcard.fn?.value?.split(' ').slice(1).join(' ') || undefined,\n      org:       orgId,\n    }\n  },\n}\n```\n\nTYPE values are normalized to lowercase by the parser (`CELL` → `cell`). When serializing, the value is written as-is (`TYPE=cell`).\n\n## Hidden fields\n\nThe plugin injects a `cardDavSync` group field into the configured collection:\n\n| Field | Purpose |\n|---|---|\n| `cardDavSync.uid` | vCard `UID` — stable remote identifier |\n| `cardDavSync.url` | Full resource URL on the CardDAV server |\n| `cardDavSync.etag` | Last known ETag for change detection |\n| `cardDavSync.lastSyncedAt` | Timestamp of last successful sync |\n\nAll fields are hidden in the admin UI by default. Set `debug: true` to make them visible.\n\n## Soft deletes\n\nIf your collection has `trash: true`, Payload treats deletes as updates that set `deletedAt` — the plugin handles this automatically.\n\n| Action | What happens |\n|---|---|\n| Trash a contact in Payload | Contact deleted from CardDAV; `cardDavSync` fields cleared |\n| Restore a trashed contact | Contact recreated on CardDAV from Payload fields only (see warning below) |\n| Empty trash (hard delete) | `afterDelete` hook fires; contact deleted from CardDAV |\n\n**Payload → CardDAV:** the plugin detects the soft-delete transition by comparing `doc.deletedAt` to `previousDoc.deletedAt` in the `afterChange` hook.\n\n**CardDAV → Payload (scheduled sync):** when a contact is removed from CardDAV, the plugin checks `req.payload.collections[slug].config.trash` at runtime. If `trash: true`, it updates the document with `deletedAt` (soft-delete). Otherwise it hard-deletes. No extra config needed — the plugin reads the collection config automatically.\n\n> **Warning: restoring a trashed contact does not restore the original CardDAV data.**\n>\n> When a contact is trashed, it is deleted from CardDAV and the sync state (`cardDavSync`) is cleared. If the contact is later restored in Payload, the plugin recreates it on CardDAV using only the fields returned by `toVCard(doc)` — i.e. whatever is stored in your Payload collection at that point.\n>\n> Any vCard data that was not mapped to a Payload field (birthday, address, X- extensions, etc.) is permanently lost on restore. If preserving the full vCard is important, consider keeping the original fields in the collection and mapping them through via `fromVCard` / `toVCard`.\n\n## `disabled` option\n\nSetting `disabled: true` keeps the `cardDavSync` fields in the schema (important for migrations) but disables all hooks and the sync job.\n\n## TypeScript\n\nPass your generated Payload type to get typed `toVCard` / `fromVCard`:\n\n```ts\nimport type { Contact } from './payload-types'\nimport type { VCardEntry } from '@10xmedia/payload-carddav-sync'\n\npayloadCarddavSync<Contact>({ /* ... */ })\n```\n\nImport `VCardEntry` when you need to annotate values in `toVCard`:\n\n```ts\ntoVCard: (doc, req): VCardProperties => ({\n  fn:  { value: doc.name },\n  tel: doc.phones.map((p): VCardEntry => ({ value: p.number, type: p.type })),\n})\n```\n\n## Sync behavior summary\n\n| Event | What happens |\n|---|---|\n| Create doc in Payload | PUT to CardDAV, ETag and URL stored |\n| Update doc in Payload | PUT to CardDAV with `If-Match`; on 412 force-overwrites (Payload wins) |\n| Trash doc in Payload (`trash: true`) | DELETE from CardDAV, `cardDavSync` cleared |\n| Restore trashed doc | Contact recreated on CardDAV with a new UID |\n| Delete doc in Payload (hard delete) | DELETE from CardDAV |\n| CardDAV ETag changed | Next scheduled sync updates Payload doc |\n| Contact deleted from CardDAV | Next scheduled sync: soft-deletes Payload doc if `trash: true`, otherwise hard-deletes |\n| New contact in CardDAV | Next scheduled sync creates Payload doc |\n","readmeFilename":"README.md"}