> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mercurjs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Fields

> Extend any Medusa entity with additional fields without modifying core code.

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`.

<Info>
  **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.
</Info>

## When to use Custom Fields

<Tip>
  **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`.
</Tip>

<Warning>
  **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](/rc/learn/blocks) already ships it).
</Warning>

## 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

<Steps>
  <Step title="Define your fields in medusa-config.ts">
    Register the Custom Fields module and describe your fields in the `customFields` option:

    ```ts medusa-config.ts theme={null}
    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",
                },
              },
            },
          },
        },
      ],
    });
    ```

    <Warning>
      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.
    </Warning>
  </Step>

  <Step title="Run migrations">
    ```bash theme={null}
    bunx medusa db:migrate
    ```

    The module generates and applies the schema for each side table automatically.
  </Step>

  <Step title="Query the fields">
    Custom fields are automatically linked to their parent entity. Retrieve them with Medusa's remote query:

    ```ts theme={null}
    const products = await query.graph({
      entity: "product",
      fields: ["id", "title", "custom_fields.*"],
    });
    ```
  </Step>
</Steps>

## Supported field types

| Type       | Description                                               |
| ---------- | --------------------------------------------------------- |
| `string`   | Short text                                                |
| `text`     | Long text                                                 |
| `integer`  | Whole number                                              |
| `float`    | Decimal number                                            |
| `boolean`  | True/false                                                |
| `date`     | Date only                                                 |
| `time`     | Time only                                                 |
| `datetime` | Date and time                                             |
| `json`     | JSON object                                               |
| `array`    | Array of values                                           |
| `enum`     | One of a predefined set of values (requires `enum` array) |

### Field options

| Option         | Type       | Default | Description                                      |
| -------------- | ---------- | ------- | ------------------------------------------------ |
| `nullable`     | `boolean`  | `true`  | Whether the field can be null                    |
| `defaultValue` | `any`      | `null`  | Default value for the field                      |
| `enum`         | `string[]` | —       | 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:

```ts theme={null}
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:

```ts theme={null}
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

<AccordionGroup>
  <Accordion title="Can I add or change fields after the first migration?">
    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.
  </Accordion>

  <Accordion title="How do vendors or admins edit these values from the panels?">
    Through your own UI: add a page or re-compose an existing one ([extending panels](/rc/resources/customization/extending-panels)) and call an API route that uses the service's `upsert` — or compose `upsertCustomFieldsStep` into a workflow behind a [custom route](/rc/resources/tutorials/custom-api-route).
  </Accordion>

  <Accordion title="Can one entity have multiple custom-field records?">
    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.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Add a custom API route" href="/rc/resources/tutorials/custom-api-route">
    Expose your custom fields through a typed endpoint.
  </Card>

  <Card title="Extend a workflow" href="/rc/resources/customization/extend-a-workflow">
    Write custom fields from inside existing business flows.
  </Card>
</CardGroup>
