Skip to main content
Custom Fields let you attach extra data to any existing Medusa entity — products, customers, orders, and more — through configuration alone. No migrations to write, no models to define. The module handles table creation and schema updates automatically when you run db:migrate.
Where this sits in the stack. Custom Fields is a Mercur module (@mercurjs/core/modules/custom-fields) layered on top of Medusa’s standard module-link system — it generates the side table, the link, and the schema for you. It’s the lightest way to add data to an entity.

When to use Custom Fields

Use Custom Fields when the data is a plain property of an existing entity — it belongs to exactly one record, has no lifecycle of its own, and you mostly read it alongside its parent. Typical cases:
  • A brand, warranty_months, or is_featured flag on a product
  • A company_name or loyalty tier on a customer
  • An internal source or priority tag on an order
You get a typed column, a default value, and queryability through query.graph — for the cost of one config entry and a db:migrate.
Build a custom Medusa module instead when the data outgrows “extra columns”:
  • It has its own lifecycle — created, updated, and deleted independently of the parent (e.g. reviews, support tickets)
  • It has relations to more than one entity, or many records per parent
  • It carries business logic — validation rules, state transitions, workflows
  • You need its own API routes and permissions around it
Custom Fields is strictly one row per parent entity. Forcing a one-to-many or stateful concept into a JSON field works until it doesn’t — model it as a module (or check whether a block already ships it).

How it works

The Custom Fields module creates a separate table for each entity you extend (e.g. product_custom_fields). Each table is linked back to the original entity via a foreign key with a unique constraint, ensuring a one-to-one relationship. The module automatically registers these links so you can query custom fields through Medusa’s standard remote query.

Set up custom fields

1

Define your fields in medusa-config.ts

Register the Custom Fields module and describe your fields in the customFields option:
medusa-config.ts
import { Modules } from "@medusajs/framework/utils";

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@mercurjs/core/modules/custom-fields",
      options: {
        customFields: {
          Product: {
            brand: { type: "string", nullable: true },
            is_featured: { type: "boolean", defaultValue: false },
            weight: { type: "float", nullable: true },
          },
          Customer: {
            company_name: { type: "string", nullable: true },
            tier: {
              type: "enum",
              enum: ["bronze", "silver", "gold"],
              defaultValue: "bronze",
            },
          },
        },
      },
    },
  ],
});
The key in customFields (e.g. Product, Customer) must match the entity name as registered in Medusa’s joiner configuration — a mismatched name fails at link resolution, not at config load, so double-check spelling and casing.
2

Run migrations

bunx medusa db:migrate
The module generates and applies the schema for each side table automatically.
3

Query the fields

Custom fields are automatically linked to their parent entity. Retrieve them with Medusa’s remote query:
const products = await query.graph({
  entity: "product",
  fields: ["id", "title", "custom_fields.*"],
});

Supported field types

TypeDescription
stringShort text
textLong text
integerWhole number
floatDecimal number
booleanTrue/false
dateDate only
timeTime only
datetimeDate and time
jsonJSON object
arrayArray of values
enumOne of a predefined set of values (requires enum array)

Field options

OptionTypeDefaultDescription
nullablebooleantrueWhether the field can be null
defaultValueanynullDefault value for the field
enumstring[]Required for enum type. List of allowed values

Using the service directly

The Custom Fields module exposes a service with upsert, delete, and list methods. You can use these in your own workflows and API routes:
import { MercurModules } from "@mercurjs/types";

// In a step or API route
const customFieldsService = container.resolve(MercurModules.CUSTOM_FIELDS);

// Create or update custom fields for a product
await customFieldsService.upsert("product", {
  id: "prod_123",
  brand: "Acme",
  is_featured: true,
});

// Delete custom fields by record ID
await customFieldsService.delete("product", ["cf_456"]);
The upsert method accepts either a single object or an array. If a record already exists for the given entity ID, it updates it; otherwise, it creates a new one.

Workflow steps

The module ships with two workflow steps you can compose into your own workflows:
import {
  upsertCustomFieldsStep,
  deleteCustomFieldsStep,
} from "@mercurjs/core/workflows";
  • upsertCustomFieldsStep — Takes { alias, data } where alias is the entity name (e.g. "product") and data contains the entity ID and field values.
  • deleteCustomFieldsStep — Takes { alias, ids } to soft-delete custom field records by their IDs.

FAQ

Yes — edit the customFields config and run db:migrate again; the module updates the side table’s schema. Removing a field from config stops exposing it; treat destructive column changes with the same care as any schema migration.
Through your own UI: add a page or re-compose an existing one (extending panels) and call an API route that uses the service’s upsert — or compose upsertCustomFieldsStep into a workflow behind a custom route.
No — the side table has a unique constraint on the parent ID, enforcing one-to-one. If you need many records per parent, that’s a custom module, not custom fields.

Next steps

Add a custom API route

Expose your custom fields through a typed endpoint.

Extend a workflow

Write custom fields from inside existing business flows.