> ## Documentation Index
> Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Extensions Overview

> Extend LaunchMyStore with custom UI for storefront, checkout, and admin

# 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](/functions/overview) — those run
server-side and return JSON, not UI.

## Extension Types

<CardGroup cols={2}>
  <Card title="Storefront Blocks" icon="puzzle-piece" href="/extensions/theme-blocks">
    Liquid blocks merchants drop into theme sections (e.g. product page,
    homepage).
  </Card>

  <Card title="Checkout UI" icon="credit-card" href="/extensions/checkout-ui">
    Sandboxed iframe rendered at a named slot inside the checkout.
    Talks back to the host via App Bridge.
  </Card>

  <Card title="Post-Purchase" icon="check-circle" href="/extensions/post-purchase">
    Iframe rendered on the order status (thank-you) page right after checkout. Smaller wired-action set
    than checkout UI.
  </Card>

  <Card title="Admin Blocks" icon="gauge" href="/extensions/admin-blocks">
    Iframe embedded on admin resource detail pages (product, order,
    customer, etc.).
  </Card>

  <Card title="Admin Actions" icon="bolt" href="/extensions/admin-actions">
    Button on an admin resource page that opens an iframe modal.
  </Card>

  <Card title="Admin Print Actions" icon="print" href="/extensions/admin-print-actions">
    Generate printable templates for orders, invoices, packing slips.
  </Card>

  <Card title="Email Templates" icon="envelope" href="/extensions/email-templates">
    Override the merchant's transactional emails for named events.
  </Card>

  <Card title="App Scripts" icon="code" href="/extensions/app-scripts">
    JavaScript files auto-injected on every storefront page — pixels,
    chat widgets, floating buttons. No theme edits.
  </Card>

  <Card title="Web Pixels" icon="chart-line" href="/extensions/web-pixel">
    Sandboxed customer-event listeners (`page_viewed`, `cart_updated`,
    `checkout_completed`).
  </Card>

  <Card title="Customer Account" icon="user" href="/extensions/customer-account">
    Iframe blocks on the new customer-account dashboard, order status,
    profile, and order list pages.
  </Card>

  <Card title="Order Routing" icon="route" href="/extensions/order-routing">
    Rules that pick the fulfillment location for an order at checkout.
  </Card>

  <Card title="App Proxy" icon="diagram-project" href="/extensions/app-proxy">
    Serve dynamic content from your app server on the merchant's own
    domain — tracking widgets, account pages, AJAX endpoints.
  </Card>
</CardGroup>

Other surfaces declared in `app.json`:

* **[Storefront snippets](/extensions/storefront-snippets)** — reusable
  Liquid includes (`{% render 'foo' %}`).
* **[Storefront embeds](/extensions/storefront-embeds)** —
  overlay/floating scripts injected at `<head>` or before `</body>` per
  the merchant's app-embed toggle.
* **[Functions](/functions/overview)** (nine WASM function types; a tenth
  declarative `fulfillment_location_rule` is accepted at install but not yet
  dispatched at runtime) — declarative 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.

<Warning>
  **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.
</Warning>

## 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`, …).

### Accessing metafields

Your app reads and writes metafields through the OAuth Metafields API —
[`/api/v1/metafields.json`](/api-reference/metafields/list) — authenticated
with your Bearer access token and the `read_metafields` / `write_metafields`
scopes.

### Namespace convention

Use **one namespace per app**, formatted as `app_<handle_with_underscores>`:

| App handle        | Metafield namespace   |
| ----------------- | --------------------- |
| `channels-hub`    | `app_channels_hub`    |
| `foundry-reviews` | `app_foundry_reviews` |
| `restock-alerts`  | `app_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:

```liquid theme={null}
{% 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 scope                                  | `ownerType` | `ownerId`             |
| ------------------------------------------- | ----------- | --------------------- |
| Per-store app config / install settings     | `shop`      | (resolved from store) |
| Per-product counter / summary               | `product`   | product UUID          |
| Per-variant inventory note                  | `variant`   | variant UUID          |
| Per-customer state (subscription, wishlist) | `customer`  | customer UUID         |
| Per-order metadata                          | `order`     | order 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

```javascript theme={null}
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.

```javascript theme={null}
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**:

```json theme={null}
{
  "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](/extensions/admin-actions) and
[Admin Print Actions](/extensions/admin-print-actions).

## Installing Extensions

Two install modes are supported by `POST /api/apps/install-extensions`.
This is a **server-to-server** endpoint (no CORS): inline uploads require
the platform's **internal API key** and are performed by the backend as
part of the app install flow — not called directly from a browser with a
merchant token. The only browser-accepted path is Mode B (`fromCatalog:
true`) with a merchant Bearer whose store matches `domainSlug`.

### Mode A — inline upload (internal, server-to-server)

For platform-side deploys that deliver arbitrary extension files:

```javascript theme={null}
await fetch('https://store.launchmystore.io/api/apps/install-extensions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Internal-API-Key': INTERNAL_API_KEY
  },
  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:

```javascript theme={null}
await fetch('https://store.launchmystore.io/api/apps/install-extensions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    // Merchant access token — the store it owns must match domainSlug.
    Authorization: `Bearer ${accessToken}`
  },
  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. This is the one path that accepts a
merchant Bearer instead of the internal API key — the backend verifies the
merchant owns the `domainSlug` store before copying.

<Note>
  **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.
</Note>

## Uninstalling

```javascript theme={null}
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:

```json theme={null}
{
  "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
* `blog` — Blog listing
* `search` — Search results

`schema.target` is a passthrough hint (not enforced at install). Full
list: [Storefront Blocks](/extensions/theme-blocks#available-targets).

### 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](/extensions/checkout-ui#target-slots).

### Admin blocks

* `product.details.block`
* `order.details.block`
* `customer.details.block`
* `collection.details.block`
* `discount.details.block`
* 26 total — see [Admin Blocks](/extensions/admin-blocks#available-targets).

### 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](/extensions/email-templates).

## See Also

* [App Bridge Overview](/app-bridge/overview) — postMessage contract used
  by checkout, post-purchase, and admin iframes.
* [Functions Overview](/functions/overview) — declarative server-side
  business logic, not UI.
