npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tinloof/medusa-sanity-sync

v0.1.1

Published

A flexible and extensible Medusa plugin for syncing data to Sanity CMS.

Readme

@tinloof/medusa-sanity-sync

A flexible and extensible Medusa plugin for syncing data to Sanity CMS.

Features

  • Multi-entity support: Sync products, categories, collections, and custom entities
  • Single transformer: One top-level transformer function handles all entity transformations
  • Draft-aware: Updates target drafts when they exist, deletes remove both published and draft documents
  • Event batching: Configurable batching with deduplication for efficient syncing
  • Custom sync handlers: Override create/update/delete behavior (e.g., archive instead of delete)
  • Admin UI widgets: Visual sync status for all entity types
  • Non-destructive updates: Only sets fields returned by the transformer, preserving Sanity-managed fields

Installation

npm install @tinloof/medusa-sanity-sync
# or
yarn add @tinloof/medusa-sanity-sync

Quick Start

1. Configure the Plugin

Add the plugin to your medusa-config.ts:

import { defineConfig } from "@medusajs/medusa";

export default defineConfig({
  // ... other config
  plugins: [
    {
      resolve: "@tinloof/medusa-sanity-sync",
      options: {
        api_token: process.env.SANITY_API_TOKEN,
        project_id: process.env.SANITY_PROJECT_ID,
        api_version: "2024-01-01",
        dataset: "production",
        studio_url: process.env.SANITY_STUDIO_URL,
      },
    },
  ],
});

Configuration Options

| Option | Type | Required | Description | | ------------- | --------------------- | -------- | ------------------------------------------ | | api_token | string | Yes | Sanity API token with write permissions | | project_id | string | Yes | Sanity project ID | | api_version | string | Yes | Sanity API version (e.g., "2024-01-01") | | dataset | string | Yes | Sanity dataset name | | studio_url | string | No | URL to Sanity Studio for generating links | | entities | EntityDefinition[] | No | Entity configurations (defaults to all) | | transformer | TransformerFunction | No | Top-level transformer for all entity types | | batching | BatcherConfig | No | Event batching config (interval, max size) | | syncHandlers | SyncHandlers | No | Custom create/update/delete handlers |

Transformer

The transformer is a single function at the plugin level that handles transformations for all entity types:

import type { SanitySyncPluginOptions } from "@tinloof/medusa-sanity-sync";

const options: SanitySyncPluginOptions = {
  api_token: process.env.SANITY_API_TOKEN,
  project_id: process.env.SANITY_PROJECT_ID,
  api_version: "2024-01-01",
  dataset: "production",

  // Single transformer for all entities
  transformer: (entityType, data) => {
    switch (entityType) {
      case "product":
        return {
          title: data.title,
          handle: data.handle,
          description: data.description,
          images: data.images?.map((img) => ({ url: img.url })),
        };
      case "product_category":
        return {
          name: data.name,
          handle: data.handle,
        };
      case "product_collection":
        return {
          title: data.title,
          handle: data.handle,
        };
      default:
        return data;
    }
  },
};

Important: Only fields returned by the transformer are set on the Sanity document. Fields managed in Sanity (e.g., rich text, images, sections) are preserved — the plugin never unsets fields not in the transformer output.

Default Transformer

If no transformer is provided, a default transformer is used that maps basic fields:

  • Products: title, handle
  • Categories: name, handle
  • Collections: title, handle
  • Unknown entities: All fields except internal ones (id, created_at, updated_at, deleted_at)

You can import and extend the default transformer:

import { defaultTransformer } from "@tinloof/medusa-sanity-sync";

const options: SanitySyncPluginOptions = {
  // ... connection config
  transformer: (entityType, data) => {
    if (entityType === "product") {
      return {
        ...defaultTransformer(entityType, data),
        description: data.description,
        customField: data.metadata?.custom_value,
      };
    }
    return defaultTransformer(entityType, data);
  },
};

Entity Configuration

Using Default Entities

The plugin includes default configurations for products, categories, and collections with explicit query fields:

import {
  defaultProductEntity,
  defaultCategoryEntity,
  defaultCollectionEntity,
  defaultEntities,
} from "@tinloof/medusa-sanity-sync";

// Use all defaults - simplest setup
const options = {
  // ... connection config
  entities: defaultEntities,
};

// Or select specific entities
const options = {
  // ... connection config
  entities: [defaultProductEntity, defaultCategoryEntity],
};

Query Fields

Each entity definition specifies which fields to fetch from Medusa via queryFields. The default entities include sensible defaults:

  • Products: id, title, handle, subtitle, description, status, thumbnail
  • Categories: id, name, handle, description, is_active, is_internal, rank, parent_category_id
  • Collections: id, title, handle

To fetch additional fields or relations, extend the defaults:

import { defaultProductEntity } from "@tinloof/medusa-sanity-sync";

const options = {
  // ... connection config
  entities: [
    {
      ...defaultProductEntity,
      // Add relations to the existing query fields
      queryFields: [
        ...defaultProductEntity.queryFields,
        "images.*",
        "variants.*",
      ],
    },
  ],
};

Note: If queryFields is omitted, a * wildcard is used. This works for some entity types (e.g., products) but may not return all scalar fields for others (e.g., categories, collections). When in doubt, specify fields explicitly.

Custom Entity Definition

Create your own entity definitions for any Medusa data type:

import type { EntityDefinition } from "@tinloof/medusa-sanity-sync";

const myCustomEntity: EntityDefinition = {
  // Medusa entity name (used in queries)
  medusaEntity: "my_custom_entity",

  // Sanity document type
  sanityType: "customEntity",

  // Events that trigger sync
  events: [
    "my-custom-entity.created",
    "my-custom-entity.updated",
    "my-custom-entity.deleted",
  ],

  // Fields to fetch from Medusa
  queryFields: ["id", "name", "description"],
};

// Handle it in your transformer
const options = {
  // ... connection config
  transformer: (entityType, data) => {
    if (entityType === "my_custom_entity") {
      return {
        name: data.name,
        description: data.description,
      };
    }
    // ... handle other entities
  },
  entities: [myCustomEntity],
};

Draft-Aware Sync

The plugin is fully draft-aware:

  • Updates: If a Sanity draft exists (drafts.<id>), the draft is updated. Otherwise the published document is updated.
  • Deletes: Both the published document and its draft are deleted.
  • Creates: Documents are created with createIfNotExists to avoid duplicates.

This means editors can work on drafts in Sanity Studio without their changes being overwritten — Medusa synced fields stay current on whichever version is active.

Transformation Pipeline

The plugin uses a simple transformation pipeline:

  1. Transformer: The top-level transformer function transforms the data for Sanity.
  2. Sanity Metadata: _type and _id are added automatically.
Event → Batcher → Transformer → Add _type/_id → Sanity API

Subscribers

Default Subscribers

The plugin includes a unified subscriber for:

  • Products (product.created, product.updated, product.deleted)
  • Categories (product-category.created, product-category.updated, product-category.deleted)
  • Collections (product-collection.created, product-collection.updated, product-collection.deleted)

Custom Subscribers

Create subscribers for custom entities using the factory:

// src/subscribers/my-custom-entity-sync.ts
import { createEntitySubscriber } from "@tinloof/medusa-sanity-sync/subscribers";

const { handler, config } = createEntitySubscriber({
  entityType: "my_custom_entity",
  events: [
    "my-custom-entity.created",
    "my-custom-entity.updated",
    "my-custom-entity.deleted",
  ],
});

export default handler;
export { config };

Workflows

Use syncEntitiesWorkflow for syncing any entity type:

import { syncEntitiesWorkflow } from "@tinloof/medusa-sanity-sync/workflows";

// Sync specific products
await syncEntitiesWorkflow(container).run({
  input: {
    entityType: "product",
    ids: ["prod_123", "prod_456"],
  },
});

// Sync all categories
await syncEntitiesWorkflow(container).run({
  input: {
    entityType: "product_category",
  },
});

API Routes

Sync

# Sync a specific entity
POST /admin/sanity/entities/:entityType/:id/sync

# Sync all registered entities
POST /admin/sanity/syncs

# Get sync history
GET /admin/sanity/syncs

Documents

# Get a Sanity document by ID
GET /admin/sanity/documents/:id

Admin UI

Widgets

The plugin provides sync status widgets for:

  • Product details page
  • Category details page
  • Collection details page

Each widget shows:

  • Current sync status (synced/not synced)
  • Button to trigger manual sync
  • View Sanity document JSON

Sync History Page

Access the sync history at /app/sanity in the admin dashboard.

Advanced Usage

Accessing the Sanity Client

import { SANITY_MODULE } from "@tinloof/medusa-sanity-sync";

const sanityModule = container.resolve(SANITY_MODULE);

// Get the raw Sanity client
const client = sanityModule.getClient();

// Query Sanity directly
const products = await client.fetch('*[_type == "product"]');

Custom Widget for New Entity Types

// src/admin/widgets/my-entity-widget.tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk";
import { SyncWidget } from "@tinloof/medusa-sanity-sync/admin";

const MyEntityWidget = ({ data }) => (
  <SyncWidget data={data} entityType="my_custom_entity" compareField="name" />
);

export const config = defineWidgetConfig({
  zone: "my_custom_entity.details.after",
});

export default MyEntityWidget;

Event Batching

Events are batched for efficient processing. Configure batching behavior:

const options: SanitySyncPluginOptions = {
  // ... connection config
  batching: {
    flushInterval: 10000, // Flush every 10 seconds (default)
    maxBatchSize: 50, // Flush after 50 events (default)
    enabled: true, // Set false to sync immediately per event
  },
};

Deduplication

The batcher automatically deduplicates events for the same entity:

| Sequence | Result | | ---------------- | -------------------- | | create + update | create (latest data) | | create + delete | skip both | | update + update | update (latest data) | | update + delete | delete only |

Custom Sync Handlers

Override the default create/update/delete behavior:

const options: SanitySyncPluginOptions = {
  // ... connection config
  syncHandlers: {
    onCreate: async (entityType, id, data, ctx) => {
      await ctx.sanityClient.create({ _type: entityType, _id: id, ...data });
    },
    onUpdate: async (entityType, id, data, ctx) => {
      await ctx.sanityClient.patch(id).set(data).commit();
    },
    onDelete: async (entityType, id, ctx) => {
      // Archive instead of delete
      await ctx.sanityClient.patch(id).set({ archived: true }).commit();
    },
  },
};

TypeScript

Full TypeScript support with exported types:

import type {
  EntityDefinition,
  SanitySyncPluginOptions,
  SyncEntitiesInput,
  SyncResult,
  SyncHandlers,
  SyncContext,
  TransformerFunction,
  BatcherConfig,
} from "@tinloof/medusa-sanity-sync";

// Import utility functions
import { defaultTransformer } from "@tinloof/medusa-sanity-sync";

Compatibility

  • Medusa v2.12.3+
  • Node.js v20+

License

MIT