@hartl-services/medusa-payload-sync 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/.medusa/server/src/admin/index.js +26 -0
  2. package/.medusa/server/src/admin/index.mjs +27 -0
  3. package/.medusa/server/src/modules/payload-sync/domain/contracts.js +35 -0
  4. package/.medusa/server/src/modules/payload-sync/domain/errors.js +54 -0
  5. package/.medusa/server/src/modules/payload-sync/domain/options.js +43 -0
  6. package/.medusa/server/src/modules/payload-sync/domain/reconciliation.js +31 -0
  7. package/.medusa/server/src/modules/payload-sync/domain/status.js +7 -0
  8. package/.medusa/server/src/modules/payload-sync/index.js +15 -0
  9. package/.medusa/server/src/modules/payload-sync/loaders/register-payload-client.js +108 -0
  10. package/.medusa/server/src/modules/payload-sync/service.js +213 -0
  11. package/.medusa/server/src/modules/payload-sync/services/payload-rest-client.js +326 -0
  12. package/.medusa/server/src/scripts/reconcile-payload-catalog.js +160 -0
  13. package/.medusa/server/src/subscribers/payload-sync-product-deleted.js +18 -0
  14. package/.medusa/server/src/subscribers/payload-sync-product-upserted.js +18 -0
  15. package/.medusa/server/src/subscribers/payload-sync-variant-deleted.js +18 -0
  16. package/.medusa/server/src/subscribers/payload-sync-variant-upserted.js +21 -0
  17. package/.medusa/server/src/types/index.js +18 -0
  18. package/.medusa/server/src/types/public.js +3 -0
  19. package/.medusa/server/src/workflows/payload-sync/authoritative-reconciliation.js +357 -0
  20. package/.medusa/server/src/workflows/payload-sync/external-step-failure.js +14 -0
  21. package/.medusa/server/src/workflows/payload-sync/reconcile-catalog.js +103 -0
  22. package/.medusa/server/src/workflows/payload-sync/sync-product.js +78 -0
  23. package/.medusa/server/src/workflows/payload-sync/sync-variant.js +129 -0
  24. package/.medusa/server/src/workflows/payload-sync/unpublish-product.js +36 -0
  25. package/.medusa/server/src/workflows/payload-sync/unpublish-variant.js +36 -0
  26. package/LICENSE +21 -0
  27. package/README.md +507 -0
  28. package/package.json +52 -0
package/README.md ADDED
@@ -0,0 +1,507 @@
1
+ # Medusa Payload Sync
2
+
3
+ This Medusa shop component mirrors the small set of Medusa product
4
+ and variant facts needed by Payload projects. Medusa remains the source of truth
5
+ for catalog and commerce data. Payload remains the source of truth for editorial
6
+ content.
7
+
8
+ The plugin writes only two hidden, protected mirror collections. Editorial
9
+ documents live in separate, project-defined collections, so publishing a Medusa
10
+ title or handle can never publish an editorial draft.
11
+
12
+ ## Support
13
+
14
+ - Medusa: `^2.19.0`
15
+ - Payload: `3.x` REST API
16
+ - Node.js: `>=20`
17
+ - Package manager: `pnpm` (`pnpm@11.5.3` in this workspace)
18
+
19
+ All Medusa 2.x peer dependencies accept `^2.19.0`; development dependencies
20
+ remain pinned to `2.19.0`.
21
+
22
+ ## Installation in a shop
23
+
24
+ ```sh
25
+ pnpm add @hartl-services/medusa-payload-sync
26
+ ```
27
+
28
+ ```ts
29
+ // medusa-config.ts
30
+ const plugins = [
31
+ {
32
+ resolve: "@hartl-services/medusa-payload-sync",
33
+ options: {
34
+ payloadUrl: process.env.PAYLOAD_URL,
35
+ payloadApiKey: process.env.PAYLOAD_SERVICE_API_KEY,
36
+ // optional, defaults to "users"
37
+ payloadAuthCollection: process.env.PAYLOAD_AUTH_COLLECTION,
38
+ },
39
+ },
40
+ // ...other plugins
41
+ ];
42
+ ```
43
+
44
+ Required options and environment variables:
45
+
46
+ - `payloadUrl` — Payload origin, `http`/`https` (`PAYLOAD_URL`).
47
+ - `payloadApiKey` — API key of the dedicated Payload service user
48
+ (`PAYLOAD_SERVICE_API_KEY`).
49
+
50
+ Everything else (`payloadAuthCollection`, `collections.product`,
51
+ `collections.variant`, `requestTimeoutMs`, `backfillBatchSize`,
52
+ `backfillBatchDelayMs`) is optional; see
53
+ [Options and exact defaults](#options-and-exact-defaults) below. Keep
54
+ `payloadApiKey` out of any public/browser environment-variable prefix and
55
+ never forward it to a Storefront or Admin client. Also complete
56
+ [Configure Payload authentication](#configure-payload-authentication) below,
57
+ which the plugin itself does not automate.
58
+
59
+ ## Configure Medusa
60
+
61
+ This repository's demo/test host, `apps/backend`, configures Payload
62
+ Sync as a local workspace dependency directly in `apps/backend/medusa-config.ts`,
63
+ following the same direct `medusa-config.ts` entry shown above; it does not
64
+ ship a Payload runtime package.
65
+
66
+ Feature selection and non-secret defaults belong directly in the plugin's
67
+ options in `apps/backend/medusa-config.ts`. It is enabled by being listed
68
+ there, not by a separate runtime feature flag. `medusa-config.ts` selects the
69
+ default Payload auth collection `users`, overridable via
70
+ `PAYLOAD_AUTH_COLLECTION`.
71
+
72
+ Dokploy must provide the server-only runtime values `PAYLOAD_URL` and
73
+ `PAYLOAD_SERVICE_API_KEY`; `PAYLOAD_AUTH_COLLECTION` can override the
74
+ default. `apps/backend/medusa-config.ts` validates and passes those values to
75
+ the local plugin. Never use a public/browser environment-variable prefix for
76
+ the API key or forward it to a Storefront or Admin client.
77
+
78
+ The backend configuration owns these server-only options directly; Payload
79
+ Sync exports no consumer-facing option or HTTP contract.
80
+
81
+ ### Options and exact defaults
82
+
83
+ | Option | Required | Implemented default | Purpose |
84
+ | ----------------------- | -------- | ------------------------- | ---------------------------------------------------------------------------------------------- |
85
+ | `payloadUrl` | yes | none | Payload origin using `http` or `https`; one trailing slash is removed. The client adds `/api`. |
86
+ | `payloadApiKey` | yes | none | API key of the dedicated Payload service user. |
87
+ | `payloadAuthCollection` | no | `users` | Payload auth collection slug used in the API-key Authorization header. |
88
+ | `collections.product` | no | `medusa-products` | Product mirror collection slug. |
89
+ | `collections.variant` | no | `medusa-product-variants` | Variant mirror collection slug. |
90
+ | `requestTimeoutMs` | no | `10000` | Timeout for one Payload REST request; accepted range `100`–`120000`. |
91
+ | `backfillBatchSize` | no | `50` | Medusa/Payload page size for reconciliation; accepted range `1`–`500`. |
92
+ | `backfillBatchDelayMs` | no | `250` | Delay between non-final Medusa product pages; accepted range `0`–`60000`. |
93
+
94
+ Collection and auth slugs must be lowercase Payload-style slugs.
95
+ `apps/backend/medusa-config.ts` currently forwards only the resolved URL,
96
+ API key, and auth collection; changes to non-secret shop defaults must be
97
+ modeled directly in the plugin's options there.
98
+
99
+ The REST client rejects any successful JSON response larger than 8 MiB with
100
+ `PAYLOAD_RESPONSE_TOO_LARGE` and cancels the response stream. This fixed bound
101
+ keeps a malformed or unexpectedly expanded Payload response from consuming
102
+ unbounded memory while leaving ample room for `/api/access` and the maximum
103
+ configured page of 500 deliberately minimal mirror documents. Keep editorial
104
+ and other large fields out of the mirror collections rather than relying on the
105
+ limit as storage capacity.
106
+
107
+ ## Configure Payload authentication
108
+
109
+ Create a dedicated user in one Payload auth collection and enable Payload's
110
+ standard API-key strategy on that collection. For the default auth slug:
111
+
112
+ ```ts
113
+ import type { CollectionConfig } from "payload";
114
+
115
+ export const Users: CollectionConfig = {
116
+ slug: "users",
117
+ auth: {
118
+ useAPIKey: true,
119
+ },
120
+ fields: [
121
+ // project-specific user fields
122
+ ],
123
+ };
124
+ ```
125
+
126
+ After restarting Payload with `useAPIKey: true`:
127
+
128
+ 1. Create one account dedicated to this integration in the configured auth
129
+ collection through Payload's normal Admin or Local API.
130
+ 2. Enable API-key authentication for that account and copy the generated key
131
+ once into the Medusa server's secret store.
132
+ 3. Copy the account's Payload document ID into the Payload server environment.
133
+
134
+ Keep the resulting values server-side:
135
+
136
+ - its document ID as `PAYLOAD_SYNC_SERVICE_USER_ID` in the Payload process;
137
+ - its API key as `PAYLOAD_SERVICE_API_KEY` in the Medusa process.
138
+
139
+ Set `payloadAuthCollection` to the auth collection's actual slug. The plugin
140
+ sends the standard header
141
+ `Authorization: <payloadAuthCollection> API-Key <payloadApiKey>` on every
142
+ Payload request. It never logs this header or the key. Do not put either secret
143
+ in a public environment variable, client bundle, URL, or checked-in `.env`
144
+ file.
145
+
146
+ No custom Payload endpoint, hook, field factory, or companion package is
147
+ required.
148
+
149
+ ## Payload mirror collection contract
150
+
151
+ Both mirror collections enable Payload drafts, stay hidden from the main Admin
152
+ navigation, deny every delete, and give only the dedicated service user create
153
+ and update access. Any authenticated Payload user may read them. Anonymous
154
+ reads receive only published documents through an explicit `_status` filter.
155
+
156
+ Place the shared helpers in a server-only Payload source file:
157
+
158
+ ```ts
159
+ // payload-sync-access.ts
160
+ import type { Access, FieldAccess } from "payload";
161
+
162
+ export const isPayloadSyncService: Access = ({ req }) =>
163
+ Boolean(
164
+ req.user?.id &&
165
+ String(req.user.id) === process.env.PAYLOAD_SYNC_SERVICE_USER_ID,
166
+ );
167
+
168
+ export const syncFieldUpdate: FieldAccess = ({ req }) =>
169
+ Boolean(
170
+ req.user?.id &&
171
+ String(req.user.id) === process.env.PAYLOAD_SYNC_SERVICE_USER_ID,
172
+ );
173
+ ```
174
+
175
+ `PAYLOAD_SYNC_SERVICE_USER_ID` must be set before Payload starts. A missing or
176
+ incorrect ID denies mirror writes.
177
+
178
+ ### Product mirror
179
+
180
+ ```ts
181
+ // collections/MedusaProducts.ts
182
+ import type { CollectionConfig } from "payload";
183
+
184
+ import { isPayloadSyncService, syncFieldUpdate } from "../payload-sync-access";
185
+
186
+ export const MedusaProducts: CollectionConfig = {
187
+ slug: "medusa-products",
188
+ admin: { hidden: true, useAsTitle: "title" },
189
+ versions: { drafts: true },
190
+ access: {
191
+ read: ({ req }) => (req.user ? true : { _status: { equals: "published" } }),
192
+ create: isPayloadSyncService,
193
+ update: isPayloadSyncService,
194
+ delete: () => false,
195
+ },
196
+ fields: [
197
+ {
198
+ name: "medusaId",
199
+ type: "text",
200
+ required: true,
201
+ unique: true,
202
+ index: true,
203
+ admin: { readOnly: true },
204
+ access: { update: syncFieldUpdate },
205
+ },
206
+ {
207
+ name: "title",
208
+ type: "text",
209
+ required: true,
210
+ admin: { readOnly: true },
211
+ access: { update: syncFieldUpdate },
212
+ },
213
+ {
214
+ name: "handle",
215
+ type: "text",
216
+ required: true,
217
+ index: true,
218
+ admin: { readOnly: true },
219
+ access: { update: syncFieldUpdate },
220
+ },
221
+ ],
222
+ };
223
+ ```
224
+
225
+ `medusaId` is the only identity key and must stay unique. `handle` is
226
+ intentionally **not unique**. If a deleted Medusa product is later replaced by
227
+ a new product using the old handle, its new `medusaId` identifies a new mirror
228
+ document. A title or handle change with the same `medusaId` updates the existing
229
+ document and Payload records the revision.
230
+
231
+ Field-level access does not replace collection access: the service user still
232
+ needs collection-level `update` permission for field updates to succeed.
233
+
234
+ ### Variant mirror
235
+
236
+ ```ts
237
+ // collections/MedusaProductVariants.ts
238
+ import type { CollectionConfig } from "payload";
239
+
240
+ import { isPayloadSyncService, syncFieldUpdate } from "../payload-sync-access";
241
+
242
+ export const MedusaProductVariants: CollectionConfig = {
243
+ slug: "medusa-product-variants",
244
+ admin: { hidden: true, useAsTitle: "title" },
245
+ versions: { drafts: true },
246
+ access: {
247
+ read: ({ req }) => (req.user ? true : { _status: { equals: "published" } }),
248
+ create: isPayloadSyncService,
249
+ update: isPayloadSyncService,
250
+ delete: () => false,
251
+ },
252
+ fields: [
253
+ {
254
+ name: "medusaId",
255
+ type: "text",
256
+ required: true,
257
+ unique: true,
258
+ index: true,
259
+ admin: { readOnly: true },
260
+ access: { update: syncFieldUpdate },
261
+ },
262
+ {
263
+ name: "product",
264
+ type: "relationship",
265
+ relationTo: "medusa-products",
266
+ required: true,
267
+ index: true,
268
+ admin: { readOnly: true },
269
+ access: { update: syncFieldUpdate },
270
+ },
271
+ {
272
+ name: "title",
273
+ type: "text",
274
+ required: true,
275
+ admin: { readOnly: true },
276
+ access: { update: syncFieldUpdate },
277
+ },
278
+ ],
279
+ };
280
+ ```
281
+
282
+ When `collections.product` or `collections.variant` uses a custom slug, change
283
+ the corresponding Payload `slug` values and change the variant `relationTo` to
284
+ the configured product mirror slug as well.
285
+
286
+ Register `Users`, `MedusaProducts`, and `MedusaProductVariants` in the project's
287
+ normal `payload.config.ts`. The plugin does not create or migrate these
288
+ collections.
289
+
290
+ ## Editorial isolation
291
+
292
+ Mirror documents contain only the plugin-owned facts shown above. Do not add
293
+ editorial Rich Text, SEO, media, compliance, or other mutable content fields to
294
+ them. Create any editorial collections your project needs and relate them to
295
+ the product or variant mirror using normal Payload relationships.
296
+
297
+ The plugin neither requires nor inspects those editorial collection names,
298
+ fields, access rules, relationships, drafts, or publishing workflow. It never
299
+ reads or writes editorial documents. This is the key editorial safety property:
300
+ Medusa immediately updates and publishes only hidden mirror facts, so it cannot
301
+ publish or overwrite an in-progress editorial draft.
302
+
303
+ ## State and publishing invariant
304
+
305
+ Every mirror write sends `_status: "draft"` or `_status: "published"`
306
+ explicitly. The plugin does not use `draft=true` as a status transition and
307
+ never hard-deletes a Payload document.
308
+
309
+ | Medusa state/action | Product mirror | Current variant mirrors | Historical/deleted variant mirrors | Editorial documents |
310
+ | ----------------------- | --------------------- | ----------------------------- | ---------------------------------- | ------------------- |
311
+ | Published create/update | published immediately | published immediately | draft | untouched |
312
+ | draft/proposed/rejected | draft immediately | draft immediately | draft | untouched |
313
+ | Product delete | draft | draft | draft | untouched |
314
+ | Product republish | published immediately | published immediately | draft | untouched |
315
+ | Variant delete | unchanged | deleted variant becomes draft | draft | untouched |
316
+
317
+ On every product sync, the plugin queries the current Medusa variants. A
318
+ published product publishes exactly those current variants and drafts mirror
319
+ variants no longer present in Medusa. Republish therefore restores all current
320
+ variants but never resurrects historical or deleted variants. A draft,
321
+ proposed, rejected, or deleted product drafts its product mirror and all related
322
+ variant mirrors immediately.
323
+
324
+ A standalone variant sync first ensures the parent product mirror exists and
325
+ uses its Payload document ID for the required relationship. The variant follows
326
+ the current Medusa parent product status.
327
+
328
+ Repeated events are idempotent by `medusaId`. Payload's required unique index is
329
+ the concurrency guard for parallel creates. Payload 3.87 reports that race as a
330
+ structured HTTP 400 `ValidationError`; the client recognizes only the exact
331
+ configured-collection `medusaId` shape, reads the winner once, and updates it
332
+ with the requesting snapshot. Legacy HTTP 409 conflicts remain compatible.
333
+ Other validation errors remain ordinary permanent 4xx failures. Safe repeat
334
+ processing may create additional Payload versions, but not additional mirror
335
+ identities.
336
+
337
+ ### Ordering and locking contract
338
+
339
+ Product sync, variant sync, product deletion, variant deletion, and both orphan
340
+ reconciliation page types share one coarse `payload-sync:catalog` lock. After
341
+ acquiring it, the workflow reads the authoritative Medusa entity again and
342
+ then performs the required Payload writes before releasing the lock. A delayed
343
+ old event therefore converges to the current Medusa state instead of winning
344
+ merely because its Payload request completed last.
345
+
346
+ The plugin uses Medusa 2.19.0's public owner-aware `acquire` and `release`
347
+ operations rather than `locking.execute`. Every attempt has a random owner ID,
348
+ a 15-second acquisition deadline, and a 30-second expiring lease. While the
349
+ authoritative callback is active, the same owner renews the lease every 10
350
+ seconds. Lease continuity is tracked conservatively from the start of each
351
+ successful provider call. A heartbeat that starts at or after the last
352
+ confirmed deadline is not sent, and a renewal first observed at or after that
353
+ deadline is not accepted as continuity even if the provider reports success.
354
+
355
+ Individual acquire, renew, and release provider calls are locally observed for
356
+ at most their configured deadline. A timed-out provider promise cannot be
357
+ cancelled; if it later acquires or renews a lease, cleanup uses only that
358
+ attempt's original owner ID and is itself locally observed for a finite period.
359
+ An unresolved cleanup can therefore remain in flight, but it cannot delete a
360
+ successor owner's lease and the provider-side lease remains finite. The
361
+ heartbeat timer is cancelled before the normal owner-specific release,
362
+ including error paths. A stopped process therefore leaves a finite lease rather
363
+ than an ownerless permanent lock.
364
+
365
+ If renewal or release ownership is lost while a callback is already active,
366
+ the callback is allowed to settle, its stale owner cannot release a successor's
367
+ lease, and the plugin acquires a fresh owner for one more complete authoritative
368
+ read-and-write pass. The original retryable lifecycle error is still propagated
369
+ to the workflow retry boundary even when that recovery pass encounters a
370
+ permanent Payload error. This means an effect that ran after lease loss is not
371
+ accepted as the final completer; operators can still observe a transient state
372
+ until the recovery/retry pass finishes. Lock lifecycle failures use redacted
373
+ structured error codes and are never treated as permanent Payload contract
374
+ failures.
375
+
376
+ Medusa's default in-memory locking provider coordinates only one backend
377
+ process. A deployment with multiple workers or backend replicas must configure
378
+ a shared provider that implements Medusa's owner-ID checks, same-owner lease
379
+ renewal, expiry takeover, and owner-specific release. Medusa 2.19.0's Redis
380
+ locking provider has those semantics when every process uses the same Redis
381
+ instance. The installed Medusa 2.19.0 PostgreSQL locking provider does not let a
382
+ different owner take over its expired owner row through `acquire`, so it does
383
+ not satisfy this plugin's process-death recovery contract. Reconciliation must
384
+ use the same compatible provider and lock domain as event workers. Otherwise
385
+ the ordering guarantee is only process-local or does not apply.
386
+
387
+ ## Reduced startup capability check
388
+
389
+ At normal startup, the plugin calls Payload's standard authenticated
390
+ `/api/access` endpoint. It validates:
391
+
392
+ - reachable authentication;
393
+ - both configured collection slugs;
394
+ - collection-level `read`, `create`, and `update` capabilities;
395
+ - required field capabilities only when Payload exposes those field shapes.
396
+
397
+ This is intentionally a reduced preflight. With no custom Payload code, the
398
+ standard endpoint cannot prove every field type, `unique`/index rule,
399
+ relationship `relationTo`, drafts/version setting, delete ACL, or omitted field
400
+ shape. The copyable schema above is the authoritative configuration contract;
401
+ runtime writes and the real Payload contract test are the authoritative
402
+ protocol gate.
403
+
404
+ Recognizable auth, schema, permission, and other non-retryable contract errors
405
+ fail startup with the affected collection or capability. Temporary network
406
+ failures, timeouts, HTTP 408, 429, and 5xx responses produce a structured,
407
+ redacted warning and allow startup in degraded mode; later workflows retry.
408
+ Remote preflight I/O is skipped in Medusa's data-loader-only/migration context.
409
+
410
+ ## Initial and repeat reconciliation
411
+
412
+ Reconciliation is the initial backfill and the repair tool after missed or
413
+ permanently failed events. Run the package's built script; do not invoke a raw
414
+ TypeScript source path when the same plugin is enabled in the backend, because
415
+ that can register the workflows twice.
416
+
417
+ From the shop's root, run:
418
+
419
+ ```sh
420
+ pnpm exec medusa exec \
421
+ node_modules/@hartl-services/medusa-payload-sync/.medusa/server/src/scripts/reconcile-payload-catalog.js
422
+ ```
423
+
424
+ The command uses the shop's configured database and server-only Payload
425
+ credentials.
426
+
427
+ Every run:
428
+
429
+ 1. scans Medusa products in ascending, bounded pages;
430
+ 2. invokes the normal product workflow sequentially, including current
431
+ variants;
432
+ 3. waits `backfillBatchDelayMs` only between non-final product pages;
433
+ 4. scans draft-inclusive product and variant mirrors in stable `medusaId`
434
+ order;
435
+ 5. drafts published mirror orphans whose `medusaId` no longer exists in
436
+ Medusa;
437
+ 6. logs one machine-readable summary.
438
+
439
+ The summary fields are `scannedProducts`, `syncedProducts`, `syncedVariants`,
440
+ `draftedOrphanProducts`, `draftedOrphanVariants`, and `failedProductIds`.
441
+ Individual product failures do not stop later products or orphan scans. After
442
+ all work is attempted, any non-empty `failedProductIds` makes the command exit
443
+ with an error. Automation must require an empty list.
444
+
445
+ If a multi-ID orphan page partially succeeds and then retries, that run's
446
+ drafted-orphan count can undercount mirrors that already converged on the first
447
+ attempt. Mirror state and repeat-run convergence are authoritative; this is a
448
+ metrics-only limitation.
449
+
450
+ The runner is restartable and idempotent: safe repeat runs converge on the
451
+ current Medusa state without duplicate mirror documents. Pagination is not a
452
+ database snapshot, so rerun after concurrent catalog changes. V1 deliberately
453
+ has no automatic schedule; operators decide when to rerun it.
454
+
455
+ ## Retries, logs, and incident recovery
456
+
457
+ External workflow steps use Medusa retries (`maxRetries: 3`,
458
+ `retryInterval: 2`) and idempotent writes. Transient network failures, timeouts,
459
+ HTTP 408, 429, and 5xx responses are retryable. Auth, validation, missing
460
+ capability, duplicate-identity, invalid-response, and other ordinary 4xx
461
+ contract errors are permanent, except for the handled create unique conflict.
462
+ Failed event processing is propagated rather than silently acknowledged.
463
+
464
+ Structured JSON logs include the operation and classified `code`, plus the
465
+ entity type, `medusaId`, event name, workflow/run identifiers, collection, HTTP
466
+ status, retryability, and attempt where available. Authorization values,
467
+ API-key values, and response headers are redacted. Do not add secrets to log
468
+ context or paste unredacted request data into incident records.
469
+
470
+ V1 has no plugin-owned dead-letter table. Recover an incident in this order:
471
+
472
+ 1. Inspect structured logs by `medusaId` and error `code`.
473
+ 2. Repair the authentication, collection schema/ACL, or network cause.
474
+ 3. Rerun the full reconciliation command.
475
+ 4. Require `failedProductIds: []` and confirm the expected draft/published
476
+ product and variant states.
477
+
478
+ Because reconciliation also drafts orphans, it repairs both missing updates and
479
+ missed delete events without deleting Payload history.
480
+
481
+ ## V1 out of scope
482
+
483
+ - Payload editorial/content collections, Rich Text, media, SEO, or compliance
484
+ schemas
485
+ - a Payload package, hook, custom endpoint, field factory, or automatic schema
486
+ creation
487
+ - Storefront integration or merging Medusa and Payload responses
488
+ - Medusa categories, collections, prices, inventory, Sales Channels, or other
489
+ commerce facts
490
+ - Medusa Module Links or `defineLink`
491
+ - Medusa Admin UI, an Admin reconciliation route, or an automatic schedule
492
+ - Payload hard delete or a configurable delete mode
493
+ - a plugin-owned dead-letter database/table
494
+
495
+ Projects are free to add any separate Payload collections and relationships
496
+ needed for their editorial model; those remain outside this plugin's contract.
497
+
498
+ ## Plugin development contract
499
+
500
+ Payload Sync is server-to-server integration code and intentionally exposes no
501
+ Store or Admin HTTP routes. It has no public DTO, schema, path, Storefront
502
+ client, or runtime Payload-credential contract.
503
+
504
+ If this component gains a Medusa HTTP route, define its request/query/response
505
+ DTOs with `@medusajs/framework/zod`, derive public types from those schemas,
506
+ add them and a small path object to `@hartl-services/medusa-food-supplements-contracts`, and
507
+ let consumers use their own Medusa SDK with `sdk.client.fetch<T>`.
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@hartl-services/medusa-payload-sync",
3
+ "version": "0.1.0",
4
+ "description": "Mirrors Medusa product and variant facts into Payload CMS.",
5
+ "license": "MIT",
6
+ "files": [
7
+ ".medusa/server",
8
+ "README.md",
9
+ "LICENSE"
10
+ ],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://gitlab.com/hartl-services-gmbh/services/food-supplements-e-commerce.git",
14
+ "directory": "plugins/medusa-payload-sync"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org/"
19
+ },
20
+ "sideEffects": [
21
+ "./.medusa/**"
22
+ ],
23
+ "exports": {
24
+ "./package.json": "./package.json",
25
+ "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
26
+ "./.medusa/server/src/*": "./.medusa/server/src/*"
27
+ },
28
+ "peerDependencies": {
29
+ "@medusajs/framework": "^2.19.0",
30
+ "@medusajs/medusa": "^2.19.0"
31
+ },
32
+ "devDependencies": {
33
+ "@medusajs/cli": "2.19.0",
34
+ "@medusajs/framework": "2.19.0",
35
+ "@medusajs/medusa": "2.19.0",
36
+ "@swc/core": "^1.7.28",
37
+ "@swc/jest": "^0.2.39",
38
+ "@types/jest": "^29.5.14",
39
+ "@types/node": "^20.12.11",
40
+ "jest": "^29.7.0",
41
+ "typescript": "^5.6.2"
42
+ },
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "scripts": {
47
+ "build": "medusa plugin:build",
48
+ "typecheck": "tsc --noEmit",
49
+ "test:unit": "jest --config jest.config.js",
50
+ "test:contract": "jest --config jest.contract.config.js --runInBand"
51
+ }
52
+ }