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:
- 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. - Runtime module (only if the extension has code) —
apps/api/src/extensions/runtime.tsmaps the extension id to itsExtensionRuntime:payment/cargoprovider implementations,orderInstructions(), and (viaapps/api/src/extensions/jobs.ts) job handlers named inmanifest.jobs. Provider code receives aProviderCtxwith typedsecrets— exactly the manifest-declared keys — andconfig; it never reads the database or the request. - Admin UI module (only if a panel or action needs code) —
apps/admin/src/extensions/{name}/index.tsxdefault-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
- Declare only the
scopesyou need; they appear on the consent screen verbatim. Declarepii.buyerhonestly — undeclared fields never reach your code. - Keep your config schema inside the renderable subset (booleans, numbers/money, strings/enums, one-level objects, arrays of flat objects, arrays of scalars). A registry test rejects anything the form generator cannot render.
- Mail contributions must be promo-free (
promoFree: trueis an attestation AND a runtime lint). - Respect the limits. Hooks fail open and trip a breaker; don't rely on being called.
- Never name a provider id in core code paths. If you need the platform to know something about your extension, put it in the manifest.
Generated from the live platform registries at build time — reference pages cannot go stale. Markdown variant: /building-an-extension.md