Build for Dunea
Custom data
Metafields and metaobjects: typed values on products, orders and pages, and content types the owner edits without code.
Custom data lets your plugin store structured values inside the owner's app, where the owner can see and edit them, instead of in your own database. It has two parts:
- A metafield is a typed value attached to a resource, for example
order.app--acme-gifts.gift_messageorproduct.custom.care_guide. - A metaobject is a content type with fields and entries, for example
gift_wrapwith entries "Gold", "Kraft" and "Floral".
Both are validated by the same typed-field system that block settings and automation inputs use (@dunea/plugin-sdk primitives/fields). A value that is valid in the SDK is valid in the owner's panel, in the Plugin API and on the site.
Concepts
Owners
Metafields attach to one of these resource types (METAFIELD_OWNERS):
ownerType | ownerId |
|---|---|
app | the app id (the site itself, one per app) |
product, variant, collection, order | the shop resource id |
page | a page path, e.g. /about |
funnel, funnel_step | the funnel resource id |
metaobject | a metaobject entry id |
The resource must exist in the same app. Writing a metafield on another app's product returns 404.
Field types
A definition's field is a FieldDefinition without key and label (those come from the metafield's key and name): single_line_text, multi_line_text, rich_text, integer, decimal, boolean, date, date_time, url, email, color, money ({ amount, currency } in minor units), image ({ url, alt? }), select, json (16 KB max), product_reference, variant_reference, collection_reference, metaobject_reference, page_reference. Add list: true to store up to 50 values.
References are checked: a product_reference must be a product of this app's shop, and a metaobject_reference must be an entry of the type named in validations.metaobjectType. Write the type as gift_wrap (same namespace as the field) or custom.testimonial (another namespace).
Namespaces
The namespace decides who may write:
| Namespace | Who writes |
|---|---|
custom | the owner, the Dunea AI and the owner's panel |
app--<listing slug> | only your plugin (pluginNamespace(slug)) |
a first-party id (shop, funnels…) | that Dunea plugin |
Anyone with data:read can read every namespace. The owner can also edit values in your namespace unless the definition sets access.ownerWritable: false. Definitions in your namespace (their name, fields and type) can only be changed by you.
Declaring data in your manifest
Declare what you need in the manifest's data block. Dunea creates the definitions in your namespace when the install becomes active, and updates them when a new version is activated. Uninstalling never deletes them: the owner's data stays, and a reinstall picks it up again.
import { definePlugin } from '@dunea/plugin-sdk';
export const manifest = definePlugin({
id: 'acme-gifts',
// ...
permissions: ['orders:read', 'data:read', 'data:write'],
data: {
metafields: [
{
ownerType: 'order',
key: 'gift_message',
name: 'Gift message',
field: { type: 'multi_line_text', validations: { maxLength: 500 } },
},
],
metaobjects: [
{
type: 'gift_wrap',
name: 'Gift wrap',
fields: [
{ key: 'title', label: 'Title', type: 'single_line_text', required: true },
{ key: 'swatch', label: 'Swatch', type: 'image' },
{ key: 'price', label: 'Price', type: 'money' },
],
displayField: 'title',
access: { storefront: 'public' },
},
],
},
});Declaring data requires the data:write permission. Pinned metafields (pinned, on by default) get an editor on the resource's page in the owner's panel, under Custom fields.
Themes can declare data too. A theme's types are created in custom when it is installed, and only the ones the owner doesn't have yet, so a theme never overwrites the owner's content.
Permissions
| Permission | Grants |
|---|---|
data:read | GET on definitions, metafields and metaobjects of every namespace |
data:write | writes in your own namespace only; anything else answers 403 |
API endpoints
All routes are under https://api.dunea.ai/api/plugin-api/v1 and take the install access token as a bearer token. See the Plugin API reference for authentication and errors.
| Method | Path | Permission | Body / query | Answers |
|---|---|---|---|---|
GET | /data/definitions | data:read | ?kind=metafield|metaobject&ownerType=&namespace= | definitions |
GET | /data/metafields | data:read | ?ownerType=&ownerId=&namespace=&key= | ApiMetafield[] |
PUT | /data/metafields | data:write | MetafieldSet | ApiMetafield or { deleted: true } |
DELETE | /data/metafields | data:write | ?ownerType=&ownerId=&key=&namespace= | { deleted } |
GET | /data/metaobjects | data:read | ?type=&namespace=&status=&page=&limit= | ApiMetaobject[] |
GET | /data/metaobjects/:id | data:read | ApiMetaobject | |
POST | /data/metaobjects | data:write | { type, namespace?, handle?, fields, status? } | ApiMetaobject (201) |
PATCH | /data/metaobjects/:id | data:write | { fields?, handle?, status? } | ApiMetaobject |
DELETE | /data/metaobjects/:id | data:write | { deleted: true } |
Notes:
namespacedefaults to yours on every route. To read the owner's content, passnamespace=custom.PUT /data/metafieldswithvalue: nullremoves the value.- Entries come back in the owner's manual order, 50 per page by default and up to 100 with
limit. handleis URL-safe and unique per type. Omit it and Dunea generates one from the display field (jane-doe, thenjane-doe-2). Choosing a taken handle answers409.PATCHmergesfieldsinto the current values.nullclears one field.- Invalid values answer
400with a message naming the field, for exampleGift message is too long.
interface ApiMetafield {
ownerType: MetafieldOwner;
ownerId: string;
namespace: string;
key: string;
type: string; // field type; "list.<type>" for lists
value: unknown;
updatedAt: string;
}
interface ApiMetaobject {
id: string;
type: string;
namespace: string;
handle: string;
fields: Record<string, unknown>;
status: 'draft' | 'active';
updatedAt: string;
}
interface MetafieldSet {
ownerType: MetafieldOwner;
ownerId: string;
namespace?: string; // default: your namespace
key: string;
value: unknown; // null removes it
}SDK examples
import { DuneaClient } from '@dunea/plugin-sdk';
const dunea = new DuneaClient({ accessToken });
// A buyer's gift message, saved on the order in your namespace.
await dunea.setMetafield({
ownerType: 'order',
ownerId: order.id,
key: 'gift_message',
value: 'Happy birthday, Sam!',
});
const [message] = await dunea.getMetafields('order', order.id, { namespace: 'app--acme-gifts', key: 'gift_message' });
// Fill your content type.
const gold = await dunea.createMetaobject({
type: 'gift_wrap',
fields: { title: 'Gold', price: { amount: 499, currency: 'USD' } },
});
await dunea.updateMetaobject(gold.id, { status: 'draft' });
const wraps = await dunea.listMetaobjects('gift_wrap', { status: 'active' });
// Read the owner's own content (custom namespace).
const team = await dunea.listMetaobjects('team_member', { namespace: 'custom' });
await dunea.deleteMetaobject(gold.id);
await dunea.deleteMetafield('order', order.id, 'gift_message');A missing permission throws DuneaApiError with status: 403 and missingPermission: 'data:write'.
Storefront access
The published site can read custom data only where the definition opts in with access.storefront: 'public'. The site then sees:
- metafields of public definitions, never order metafields (orders are private to the owner),
- entries of public metaobject types whose status is
active. Drafts never reach the site.
Generated sites read it with the platform hooks in src/dunea/data.tsx. Responses are cached at the edge for about 30 seconds, so the owner's edits appear without a new publish.
import { useMetaobjects, useMetafield } from '@/dunea/data';
const { entries } = useMetaobjects<{ title: string }>('gift_wrap', { namespace: 'app--acme-gifts' });
const { value: careGuide } = useMetafield<string>('product', product.id, 'care_guide');useMetaobject(type, handle) loads one entry for a detail page, and useMetafields(ownerType, ownerId) loads every public field on a resource. The Dunea AI uses these hooks when it builds pages from your content.
Limits
| Limit | Value |
|---|---|
| Definitions per app (metafields + types) | 500 |
| Declared in one manifest | 40 metafields, 20 metaobject types |
| Fields per metaobject type | 40 |
| Entries per type | 5,000 |
| Values in a list field | 50 |
json value | 16 KB |
Workbench panels
Add your own page to the Dunea workbench, identify the install with a session token, and talk to the workbench.
Blocks: sections, blocks and app embeds
Ship owner-editable sections in a plugin or theme: block anatomy, settings, presets, embeds, the import allowlist and how blocks are reviewed.