Building an extension

> Third-party submission does not exist yet (M11). Today this page documents how first-party extensions are laid out in the monorepo — the exact contract third parties will get.

A first-party extension has three homes:

  1. Manifest + config schema — an entry in packages/core/src/extensions/registry.ts: buildManifest({...}, configZod) attaches the generated JSON Schema. The Zod is the single source of truth for config validation; the manifest is what the platform and this site read.
  2. Runtime module (only if the extension has code) — apps/api/src/extensions/runtime.ts maps the extension id to its ExtensionRuntime: payment / cargo provider implementations, orderInstructions(), and (via apps/api/src/extensions/jobs.ts) job handlers named in manifest.jobs. Provider code receives a ProviderCtx with typed secrets — exactly the manifest-declared keys — and config; it never reads the database or the request.
  3. Admin UI module (only if a panel or action needs code) — apps/admin/src/extensions/{name}/index.tsx default-exports { panels: { [panelId]: Component }, actions: { [actionId]: Modal } }; zone hosts lazy-load it only when the manifest declares a panel for that zone. Extension text is module-local — it never enters the shared panel dictionary.

Everything else — settings forms, secret fields, enable/disable, ordering, the consent screen, webhook ingestion, job scheduling, CSP origins — is rendered or dispatched by the platform from the manifest.

Checklist

Generated from the live platform registries at build time — reference pages cannot go stale. Markdown variant: /building-an-extension.md