Authoring an addon

Scripts, the manifest, scopes, external entities, and the capability proxy.

You author an addon in the app at /addons/{id} (Edit → Build & Run → Save version), or over the API: POST /api/v1/addons to create, then edit its scripts, build, and map its entities. AI assistants author through the same surface with the addons_read / addons_write MCP tools. This page is the concept tour; the API reference at /api-docs is the exhaustive, always-current catalogue — including the full list of dalea.* methods.

If you haven't yet, read Addons first — it explains the sandbox and the nine scopes this page builds on.

The contract

An addon's content is a flat set of scripts.ts / .tsx files, all in one simulated directory. Import a sibling as ./its-name (no extension, no subdirectories). The limits: ≤ 512 KB per script, ≤ 32 scripts, ≤ 2 MB combined.

The entry script (manifest.sourceEntry, default index.tsx) must default-export a mount function that renders into the given element and returns an unmount cleanup:

import { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import type { AddonMount, AddonProps } from '@dalea/addon-sdk';

function App({ dalea }: AddonProps) {
  const [envs, setEnvs] = useState<{ id: string; name: string }[]>();
  const [error, setError] = useState<string>();

  useEffect(() => {
    let alive = true;
    dalea.data.environments.list()
      .then((r) => alive && setEnvs(r.environments))
      .catch((e) => alive && setError(e instanceof Error ? e.message : String(e)));
    return () => { alive = false; };
  }, [dalea]);

  if (error) return <div style={{ color: 'hsl(var(--destructive))' }}>Failed: {error}</div>;
  if (!envs) return <div>Loading…</div>;
  return <ul>{envs.map((e) => <li key={e.id}>{e.name}</li>)}</ul>;
}

const mount: AddonMount = (el, props) => {
  const root = createRoot(el);
  root.render(<App {...props} />);
  return () => root.unmount();
};

export default mount;

The mount function receives three props:

dalea
The capability proxy. Every call is async (it crosses a postMessage bridge) and mirrors the public SDK.
config
The per-instance configuration a user filled in, driven by manifest.configSchema. Read keys defensively — config.environmentId may be undefined.
grantedScopes
The scopes the user actually consented to (may be narrower than the manifest). Degrade gracefully when one is missing rather than crashing.

Allowed imports — nothing else exists

There is no npm install. The build rejects any import outside this allowlist:

  • react
  • react-dom/client
  • @dalea/ui/primitives, @dalea/ui/components, @dalea/ui/blocks — each subpath you use must be declared in manifest.ui. Prefer these over hand-rolled markup: they carry the platform's theme, spacing and accessibility, and follow the host's light/dark/brand theme automatically.
  • @dalea/addon-sdk — types only (AddonMount, AddonProps, AddonDaleaClient).
No external network, ever

The iframe's CSP blocks all fetch/XHR, and URL / data: / node: imports, filesystem traversal, and eval / new Function are all rejected. If the addon needs data it comes through dalea; if it needs an outside resource, it can't have it.

The capability proxy

dalea is the entire callable surface. Its methods mirror the public SDK — arguments are positional (id, body, params) and every call returns parsed JSON. Anything not on the surface throws ADDON_SCOPE_UNKNOWN; a listed call without its scope granted throws ADDON_SCOPE_DENIED; bursts are throttled with ADDON_RATE_LIMITED.

// data.read
const { environments } = await dalea.data.environments.list();
const { objects } = await dalea.data.tables.listObjects(tableId, { limit: 50 });

// data.write — note the deliberate asymmetry
await dalea.data.tables.createObject(tableId, { values: { name: 'S-001' } });
await dalea.data.objects.update(objectId, { values: { status: 'passed' } });
await dalea.data.objects.archive(objectId);   // there is no updateObject / deleteObject

Create a row with tables.createObject, update with objects.update, remove with objects.archive (undo via restore). The asymmetry is intentional and consistent across domains. The full method list — every path with its required scope — is in the API reference, and an assistant can fetch it live via GET /api/v1/addons/capabilities.

External entities — the process.ext contract

An addon must never hardcode a workspace id. Instead it references entities through manifest-declared variables, substituted at build time:

const tableId: string = process.ext.SAMPLES_TABLE; // a real id, injected at build
const { objects } = await dalea.data.tables.listObjects(tableId);

Every process.ext.NAME you use must be declared under manifest.externalEntities. The build errors on an undeclared name, on a raw UUID literal anywhere in source, and on computed access (process.ext[whatever]). This is exactly what makes an addon portable: when it's shared to another workspace, the installer re-maps each variable to their entities — and the description you write is the only context they get, so phrase it as an instruction. entityId: null means "declared but not mapped here" — the standard state right after installing a shared addon. It blocks the build (issue code UNMAPPED), and the entity's domain must be covered by a declared scope (a data_table needs a data.* scope).

The manifest

{
  "apiVersion": "addon/v1",
  "sourceEntry": "index.tsx",
  "scopes": ["data.read"],
  "externalEntities": {
    "SAMPLES_TABLE": {
      "entityId": null,
      "entityType": "data_table",
      "description": "The table holding the samples this addon tracks"
    }
  },
  "ui": ["primitives"],
  "surfaces": ["page", "block"],
  "configSchema": {
    "environmentId": { "type": "string", "title": "Data environment", "description": "Optional — defaults to the first environment" }
  },
  "defaultHeight": 360,
  "react": "19"
}
sourceEntry
Which script the build starts from (default index.tsx).
scopes
The subset of the nine scopes the code calls. Declare the minimum — the build warns on undeclared usage, and every extra scope is one more consent the user must grant.
externalEntities
The process.ext declarations described above.
ui
A subset of ["primitives", "components", "blocks"], covering every @dalea/ui/* subpath you import.
surfaces
page and/or block — where the addon may render.
configSchema
Powers the per-instance config form; whatever the user fills arrives as config.

You don't have to write all of this by hand. POST /api/v1/addons/{id}/suggest-manifest statically analyses your code and proposes the scopes and external entities it actually uses — review it, flesh out the descriptions, and apply.

Building & enablement

Two build endpoints: POST /api/v1/addons/{id}/build is the persisted build (scripts + manifest + your entity mappings) that must be green for the addon to render, and POST /api/v1/addons/build is a stateless compile check for a quick "does this compile?" without saving. Read the errors literally:

UNMAPPED
Not a code bug — an external entity has no entityId in this workspace. Map it.
hardcoded UUID
Move the literal id into an externalEntities variable.
scope errors
The code calls a domain the manifest doesn't declare (or declares one it never calls). Adjust scopes.
compile errors
Ordinary TypeScript/JSX errors — fix and re-save.

Remember the enablement gate: an addon created over the API or by an assistant starts disabled and won't run until a person clicks Enable on /addons/{id}. Browser-authored addons are enabled at creation. Widening the scope set later revokes enablement and requires a fresh approval.

Gotchas worth internalising
  • Always render loading and error states. Every dalea call can reject — an addon that white-screens on the first rejection is undebuggable for its users.
  • Pace loops over dalea. The per-instance rate limit is a token bucket; batch or sequence large fan-outs rather than firing hundreds of calls at once.
  • Type diagnostics are advisory. esbuild strips types, so type errors don't block the bundle — but they usually predict runtime bugs, so fix them.
  • Installed marketplace addons are read-only until you detach them — see Publishing & installing addons.

What's next