Skip to main content

Extensions

Extensions let your app inject UI or behaviour into a merchant’s store. Each extension type maps to a specific surface — storefront, checkout, post-purchase, admin — and is declared in your app’s app.json. For declarative business logic (cart transforms, discounts, shipping rates, validation), see Functions — those run server-side and return JSON, not UI.

Extension Types

Storefront Blocks

Liquid blocks merchants drop into theme sections (e.g. product page, homepage).

Checkout UI

Sandboxed iframe rendered at a named slot inside the checkout. Talks back to the host via App Bridge.

Post-Purchase

Iframe rendered on the order status (thank-you) page right after checkout. Smaller wired-action set than checkout UI.

Admin Blocks

Iframe embedded on admin resource detail pages (product, order, customer, etc.).

Admin Actions

Button on an admin resource page that opens an iframe modal.

Admin Print Actions

Generate printable templates for orders, invoices, packing slips.

Email Templates

Override the merchant’s transactional emails for named events.

App Scripts

JavaScript files auto-injected on every storefront page — pixels, chat widgets, floating buttons. No theme edits.

Web Pixels

Sandboxed customer-event listeners (page_viewed, cart_updated, checkout_completed).

Customer Account

Iframe blocks on the new customer-account dashboard, order status, profile, and order list pages.

Order Routing

Rules that pick the fulfillment location for an order at checkout.

App Proxy

Serve dynamic content from your app server on the merchant’s own domain — tracking widgets, account pages, AJAX endpoints.

MCP Provider

Register MCP tools so AI assistants can drive your app capabilities through the merchant’s MCP server.
Other surfaces declared in app.json:
  • Storefront snippets — reusable Liquid includes ({% render 'foo' %}).
  • Storefront embeds — overlay/floating scripts injected at <head> or before </body> per the merchant’s app-embed toggle.
  • Functions (nine types plus the fulfillment_location_rule dispatch wrapper) — declarative WASM modules that fire on cart verification / order placement.

How It Works

  1. Declare extensions in your app’s app.json (manifest).
  2. Install them into a merchant’s store via POST /api/apps/install-extensions — either inline (extensions: {…}) or by referencing the marketplace catalog (fromCatalog: true).
  3. The platform writes the files under extensions/{domainSlug}/{appHandle}/.
  4. The matching storefront / checkout / admin surface picks them up via /api/apps/extensions, /api/apps/checkout-extensions, /api/apps/admin-extensions, etc.

Extension Directory Layout

After install, every app lives at one path on disk:
extensions/{domainSlug}/{appHandle}/
├── app.json                           # Manifest (must exist)
├── blocks/
│   ├── product-reviews.aqua           # Liquid template
│   └── product-reviews.schema.json    # Block schema
├── snippets/
│   └── review-stars.aqua
├── assets/
│   ├── extension.css
│   └── extension.js
├── admin-actions/
│   └── refund-tool.schema.json
├── print-actions/
│   └── packing-slip.schema.json
├── email-templates/
│   └── order_confirmation.json
└── functions/
    └── shipping-rate.wasm
The host reads from this bundle through a server-side cache. After a manifest change, re-run the install call (which refreshes the cache) or wait up to 60 seconds for the manifest cache to expire.
This directory is for extension artifacts only.aqua templates, schemas, compiled .wasm functions, static CSS/JS assets. Do not write per-store runtime data (config, state, logs, counters) to disk. App runtime data lives in metafields. See the next section.

Where to store app data

Apps do not write to the platform filesystem for runtime state. Every piece of merchant- or customer-scoped data your app needs to read back later — install config, feature toggles, per-product counters, per-customer state, event logs — is persisted as a metafield. There is no app-owned database on the platform, so apps push public summaries (and private blobs) into metafields scoped to a resource (shop, product, customer, order, …).

Two ways to access metafields

CallerEndpointAuth
Third-party OAuth app/api/v1/metafields.jsonBearer access token + read_metafields / write_metafields scopes
First-party / internal helper/metafields/internal/*x-internal-api-key shared secret (server-side only)
External app developers integrate via the OAuth API. The internal channel exists for first-party catalog apps shipped with the platform — it short-circuits OAuth and resolves the merchant by domainSlug.

Namespace convention

Use one namespace per app, formatted as app_<handle_with_underscores>:
App handleMetafield namespace
channels-hubapp_channels_hub
foundry-reviewsapp_foundry_reviews
restock-alertsapp_restock_alerts
The app_* prefix is the reserved per-app namespace convention on the platform; it keeps your app’s data isolated from custom (merchant-managed) and from other apps’ namespaces. A few apps additionally publish a public summary under a second, shorter namespace so themes can read it via Aqua:
{% comment %} Private (app-internal) — read/write via metafields API {% endcomment %}
{{ product.metafields.app_foundry_reviews.reviews }}

{% comment %} Public (theme-facing) — same data, simpler key for themes {% endcomment %}
{{ product.metafields.foundry_reviews.rating }}
{{ product.metafields.foundry_reviews.count }}

Owner-type scoping

Pick the owner that matches the scope of the data:
Data scopeownerTypeownerId
Per-store app config / install settingsshop(resolved from store)
Per-product counter / summaryproductproduct UUID
Per-variant inventory notevariantvariant UUID
Per-customer state (subscription, wishlist)customercustomer UUID
Per-order metadataorderorder UUID
Compound config is fine — store the whole blob as a single type: "json" metafield under one key like config. The metafield validator decodes JSON on read so consumers get back an object.

Example: write install config

await fetch('https://api.launchmystore.io/api/v1/metafields.json', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    namespace: 'app_my_reviews',
    key: 'config',
    type: 'json',
    value: { enabled: true, theme: 'minimal', maxPerPage: 10 },
    ownerType: 'shop',
  }),
});

Example: append to a per-product log

For log-shaped data (click logs, recent reviews, subscriber lists), read the existing JSON array, append, and write back. Cap the array length to avoid unbounded growth — there is no platform-side trimming.
const existing = await getMetafield({
  namespace: 'app_foundry_reviews',
  key: 'recent',
  ownerType: 'product',
  ownerId: productId,
}) || [];

existing.push({ rating: 5, text: 'Loved it', t: Date.now() });
if (existing.length > 50) existing.splice(0, existing.length - 50);

await upsertMetafield({
  namespace: 'app_foundry_reviews',
  key: 'recent',
  type: 'json',
  value: existing,
  ownerType: 'product',
  ownerId: productId,
});

Cache invalidation

Every metafield write fires automatic cache invalidation — both the cached copy of the owning resource and any cached page HTML that rendered it. The next storefront render sees the new value. You do not need to manually invalidate anything.

What not to store as metafields

  • Large blobs (raw images, video, full PDF documents). Use the file reference type instead — upload to R2 / CDN and store the URL.
  • High-churn counters that update on every page view (view_count). Batch into hourly aggregates or use a backend counter service.
  • Cross-store data. Metafields are scoped per store; if you need global app state, run your own backend.

Manifest

app.json declares all extensions in one place. Keys under extensions are camelCase:
{
  "handle": "my-reviews-app",
  "name": "Product Reviews",
  "version": "1.0.0",
  "extensions": {
    "storefrontBlocks": [
      {
        "handle": "product-reviews",
        "title": "Product Reviews",
        "target": "product",
        "schema": { "name": "Product Reviews", "settings": [] }
      }
    ],
    "storefrontSnippets": [
      { "handle": "review-stars" }
    ],
    "storefrontEmbeds": [
      {
        "handle": "review-popup",
        "target": "body",
        "scriptSrc": "https://my-app.example.com/embed.js"
      }
    ],
    "checkoutExtensions": [
      {
        "handle": "trust-badges",
        "target": "checkout-payment-after",
        "iframeUrl": "https://my-app.example.com/checkout/trust-badges.html"
      }
    ],
    "postPurchaseExtensions": [
      {
        "handle": "upsell",
        "target": "post-purchase",
        "iframeUrl": "https://my-app.example.com/postpurchase/upsell.html"
      }
    ],
    "adminExtensions": [
      {
        "handle": "reviews-panel",
        "target": "product.details.block",
        "title": "Reviews",
        "url": "https://my-app.example.com/admin/reviews"
      }
    ],
    "appScripts": [
      {
        "id": "reviews-loader",
        "src": "/api/apps/my-reviews-app/loader",
        "loadStrategy": "defer",
        "position": "head"
      }
    ],
    "webPixels": [
      {
        "handle": "review-prompt-pixel",
        "scriptSrc": "https://my-app.example.com/web-pixel.js",
        "events": ["checkout_completed"]
      }
    ],
    "customerAccountExtensions": [
      {
        "handle": "review-list",
        "target": "customer-account.order-status.block.render",
        "iframeUrl": "https://my-app.example.com/account/reviews.html"
      }
    ],
    "functions": [
      {
        "type": "shipping_rate",
        "handle": "expedited",
        "entrypoint": "dist/shipping.wasm"
      }
    ]
  }
}
Admin actions and print actions are stored as per-handle schema files under admin-actions/ and print-actions/ — not as inline arrays in app.json. The install endpoint writes them; see Admin Actions and Admin Print Actions.

Installing Extensions

Two install modes are supported by POST /api/apps/install-extensions:

Mode A — inline upload

For third-party apps that deliver extension files via API:
await fetch('https://store.launchmystore.io/api/apps/install-extensions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${accessToken}`
  },
  body: JSON.stringify({
    domainSlug: 'merchant-store',
    appHandle: 'my-reviews-app',
    extensions: {
      blocks: [
        { handle: 'product-reviews', template: '<div>...</div>', schema: { /* ... */ } }
      ],
      snippets: [
        { handle: 'review-stars', template: '...' }
      ],
      assets: [
        { filename: 'extension.css', url: 'https://my-app.example.com/dist/extension.css' }
      ],
      adminActions: [
        {
          handle: 'refund-tool',
          target: 'admin.order-details.action.render',
          title: 'Custom refund',
          appUrl: 'https://my-app.example.com/admin/refund'
        }
      ],
      printActions: [
        {
          handle: 'packing-slip',
          target: 'admin.order-details.print.render',
          title: 'Packing slip',
          template: '<html>...</html>'
        }
      ],
      emailTemplates: [
        {
          event: 'order_confirmation',
          subject: 'Thanks for your order!',
          htmlBody: '<h1>...</h1>',
          enabled: true
        }
      ]
    }
  })
});
The handler writes:
  • blocks/{handle}.aqua + blocks/{handle}.schema.json
  • snippets/{handle}.aqua
  • assets/{filename} (fetched from URL)
  • admin-actions/{handle}.schema.json
  • print-actions/{handle}.schema.json
  • email-templates/{event}.json

Mode B — fromCatalog

For apps already published to the marketplace catalog:
await fetch('https://store.launchmystore.io/api/apps/install-extensions', {
  method: 'POST',
  body: JSON.stringify({
    domainSlug: 'merchant-store',
    appHandle: 'flash-discount',
    fromCatalog: true
  })
});
Copies app.json, blocks/, snippets/, functions/ from the catalog into the per-merchant directory.
Email templates are persisted by the platform during install. The file written here is a read-only preview — the authoritative copy is the stored template record.

Uninstalling

await fetch('https://store.launchmystore.io/api/apps/uninstall-extensions', {
  method: 'POST',
  body: JSON.stringify({
    domainSlug: 'merchant-store',
    appHandle: 'my-reviews-app'
  })
});
Removes the entire extensions/{domainSlug}/{appHandle}/ directory. Asset cache + manifest cache are invalidated automatically.

Storefront Block Schemas

Storefront blocks define settings the merchant can configure in the theme editor:
{
  "name": "Product Reviews",
  "target": "product",
  "settings": [
    { "type": "text",     "id": "title",          "label": "Section Title", "default": "Customer Reviews" },
    { "type": "checkbox", "id": "show_rating",    "label": "Show Average Rating", "default": true },
    { "type": "range",    "id": "reviews_per_page", "label": "Reviews Per Page", "min": 5, "max": 50, "step": 5, "default": 10 }
  ]
}
Inside the .aqua template, settings are available as block.settings.* (or block.<id> — both work; the Aqua engine auto-resolves to .settings.<id>).

Targets — Quick Reference

Storefront blocks

  • index — Homepage
  • product — Product pages
  • collection — Collection pages
  • cart — Cart page
  • article — Blog articles
  • page — Custom pages

Checkout UI (currently wired slots)

  • checkout-contact-after
  • checkout-shipping-after
  • checkout-shipping-method-before
  • checkout-payment-before
  • checkout-payment-after
  • checkout-order-summary-before
  • checkout-order-summary-after
  • purchase.checkout.cart-line-list.render-after
  • purchase.checkout.reductions.render-after
  • purchase.checkout.actions.render-before
  • purchase.thank-you.block.render
  • purchase.order-status.block.render
  • purchase.thank-you.cart-line-list.render-after
  • purchase.order-status.cart-line-list.render-after
Full list: Checkout UI Extensions.

Admin blocks

  • product.details.block
  • order.details.block
  • customer.details.block
  • collection.details.block
  • discount.details.block
  • 26 total — see Admin Blocks.

Admin actions

  • admin.order-details.action.render (more shipping as resource pages wire up)

Admin print actions

  • admin.order-details.print.render

Email templates

Replace built-in transactional emails for named events:
  • order_confirmation (MVP-wired)
Full event list: Email Templates.

See Also