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_message or product.custom.care_guide.
  • A metaobject is a content type with fields and entries, for example gift_wrap with 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):

ownerTypeownerId
appthe app id (the site itself, one per app)
product, variant, collection, orderthe shop resource id
pagea page path, e.g. /about
funnel, funnel_stepthe funnel resource id
metaobjecta 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:

NamespaceWho writes
customthe 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.

dunea.plugin.ts
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

PermissionGrants
data:readGET on definitions, metafields and metaobjects of every namespace
data:writewrites 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.

MethodPathPermissionBody / queryAnswers
GET/data/definitionsdata:read?kind=metafield|metaobject&ownerType=&namespace=definitions
GET/data/metafieldsdata:read?ownerType=&ownerId=&namespace=&key=ApiMetafield[]
PUT/data/metafieldsdata:writeMetafieldSetApiMetafield or { deleted: true }
DELETE/data/metafieldsdata:write?ownerType=&ownerId=&key=&namespace={ deleted }
GET/data/metaobjectsdata:read?type=&namespace=&status=&page=&limit=ApiMetaobject[]
GET/data/metaobjects/:iddata:readApiMetaobject
POST/data/metaobjectsdata:write{ type, namespace?, handle?, fields, status? }ApiMetaobject (201)
PATCH/data/metaobjects/:iddata:write{ fields?, handle?, status? }ApiMetaobject
DELETE/data/metaobjects/:iddata:write{ deleted: true }

Notes:

  • namespace defaults to yours on every route. To read the owner's content, pass namespace=custom.
  • PUT /data/metafields with value: null removes the value.
  • Entries come back in the owner's manual order, 50 per page by default and up to 100 with limit.
  • handle is URL-safe and unique per type. Omit it and Dunea generates one from the display field (jane-doe, then jane-doe-2). Choosing a taken handle answers 409.
  • PATCH merges fields into the current values. null clears one field.
  • Invalid values answer 400 with a message naming the field, for example Gift 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

LimitValue
Definitions per app (metafields + types)500
Declared in one manifest40 metafields, 20 metaobject types
Fields per metaobject type40
Entries per type5,000
Values in a list field50
json value16 KB

On this page