- Custom field definition (no code) — declare the field as data; the dashboard renders it. Right for merchant-managed attributes and most plugin data.
- Extension form field (code) — for a real database column your plugin added to
spree_products. You register the field and render its input; hydration and saving stay with the host form.
Path A: custom field definition — no code
Create a definition for products (Settings → Custom fields, the in-place “Set up” button on the Custom Fields card, or the API):custom_fields[] and persists when the merchant hits Save. Field types: short_text, long_text, rich_text, number, boolean, json.
Optional: replace the input widget
When the default widget isn’t right — a color, a rating, a structured pair — register a component for that specific definition, keyed by itsnamespace.key:
value, call onChange. Persistence stays with the card (the form’s Save on products and categories; the card’s own Save on orders and customers). When no component is registered, the default widget for the definition’s field_type renders.
Path B: extension form field — a real column
Use this when your plugin adds an actual column and API attribute.1. Backend
tech_specs, and the products controller permits it on write (see Backend integration). Read and write names must match.
2. Register the field
Tell the product form about the field: where its value comes from on load. Everything else — dirty tracking, the PATCH payload, re-baselining after save — is the host form’s job.3. Render the input from a slot
Theproduct.form_sidebar slot renders inside the product form. Bind your input to the host form with useHostForm() — no <form> of your own, no save button:
Validation
Extension fields are validated by the server — a Rails validation failing returns a 422, and the host form’s error mapping puts the message inline on your field (that’s theerrors.tech_specs read in the component). This mirrors how Vendure and Saleor treat extension-field validation: the backend is authoritative; the client renders what it says.
Which forms support this
useHostForm() works wherever a built-in form exposes its form context — currently the product (edit + new), category (edit + new), and store settings forms, with matching slots (product.form_sidebar, category.form_sidebar, store.form_main) and form keys (product, category, store). Orders and customers have no page-wide form — their sheets own their edits — so slot widgets there manage their own persistence; use useOptionalHostForm() to write a widget that adapts to both contexts.
Which path should I pick?
| Situation | Path |
|---|---|
| Merchant-defined attributes, plugin data without schema migrations | A — definition, zero code |
| Default widget is fine | A |
| A real column with Rails validations, queries, or indexes | B |
| The field belongs visually inside your own card with other UI | B |
Going further
To also surface the attribute as a table column with filtering and sorting, continue with A product attribute, end to end.Reference
- Custom fields — definitions, types, the API
- Slots — the injection mechanism
useHostFormformFieldsregistry

