> ## 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.

# App Scripts

> Inject JavaScript into every storefront page without theme edits

# App Scripts

App scripts are JavaScript files your app declares in `app.json` that the
storefront loads on every page — no theme edits, no merchant action beyond
installing the app. The host inserts a non-blocking loader in
`content_for_header` that fetches `/api/apps/extensions` and injects every
declared script via `requestIdleCallback`.

Use app scripts when your app needs to **run on the storefront** and the
effect is delivered entirely by a `<script>` (chat widgets, pixels,
typeahead overlays, floating buttons, badge swaps, etc.).

For Liquid placement merchants drag into theme sections, use
[Storefront Blocks](/extensions/theme-blocks). For sandboxed
customer-event listeners, use [Web Pixels](/extensions/web-pixel).

## When to Use App Scripts

| Need                                                           | Use                                                     |
| -------------------------------------------------------------- | ------------------------------------------------------- |
| Floating chat / WhatsApp / Calendly button on every page       | App script                                              |
| GA4 / Meta Pixel / TikTok / Hotjar / Clarity                   | App script                                              |
| Storefront-wide search typeahead, points pill, pre-order badge | App script                                              |
| Liquid block the merchant places inside a section              | [Storefront Block](/extensions/theme-blocks)            |
| Sandboxed listener for `page_viewed`, `cart_updated`, etc.     | [Web Pixel](/extensions/web-pixel)                      |
| `<head>` / `</body>` overlay the merchant can toggle per-theme | [Storefront Embed](/extensions/theme-blocks#app-embeds) |

The differentiator: app scripts load **automatically on every storefront
page** the moment the app is installed. The merchant doesn't have to drop
a block, toggle a switch, or edit the theme.

## How It Works

1. Your app's `app.json` declares one or more `appScripts` entries with a
   `src` URL.
2. After install, the storefront fetches `/api/apps/extensions?domainSlug=…`
   on every page. The response includes a `scripts[]` array merged from the
   backend (DB-backed installs) and local manifests on disk.
3. A small loader stub in `content_for_header` schedules
   `requestIdleCallback` (falls back to `setTimeout(0)`), fetches the
   extensions list, and appends one `<script>` per entry — de-duplicated by
   `src` so SPA navigations don't double-inject.
4. Your script runs on the storefront, scoped to that merchant's domain.

The loader is built into the storefront's render pipeline — you don't
need to wire anything yourself. Declaring `appScripts` in `app.json` is
enough.

## Manifest

Declare app scripts under `extensions.appScripts` in your `app.json`:

```json theme={null}
{
  "handle": "early-access",
  "name": "Early Access — Pre-orders",
  "version": "1.0.0",
  "extensions": {
    "appScripts": [
      {
        "id": "early-access-loader",
        "src": "/api/apps/early-access/loader",
        "loadStrategy": "defer",
        "position": "head"
      }
    ]
  }
}
```

### Fields

| Field          | Type                 | Default       | Description                                                                                                                                                    |
| -------------- | -------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`           | string               | —             | Unique identifier within your app. Surfaced in the loader response and on the injected `<script data-app-id="…">` tag for easy debugging.                      |
| `src`          | string               | —             | URL the loader appends as `<script src="…">`. May be a relative path on the host (e.g. `/api/apps/<handle>/loader`) or an absolute external URL. **Required.** |
| `loadStrategy` | `"defer" \| "async"` | `"defer"`     | Maps directly to the `<script>` attribute. Use `defer` when your code touches the DOM, `async` for fire-and-forget pixels.                                     |
| `position`     | `"head" \| "body"`   | `"body"`      | Where the loader appends the script tag. `head` runs marginally earlier; `body` is fine for most widgets.                                                      |
| `appId`        | string               | `<appHandle>` | Optional override surfaced on the injected `<script data-app-id>` attribute.                                                                                   |
| `config`       | object               | `{}`          | Reserved for future per-script merchant config. Not consumed by the current loader.                                                                            |

### Multiple scripts per app

Declare more than one entry if your app needs to inject a separate file at
a different position or load strategy:

```json theme={null}
{
  "extensions": {
    "appScripts": [
      { "id": "tracker",  "src": "/api/apps/my-app/tracker",  "loadStrategy": "async", "position": "head" },
      { "id": "widget",   "src": "/api/apps/my-app/widget",   "loadStrategy": "defer", "position": "body" }
    ]
  }
}
```

Both are injected on every storefront page. The loader de-duplicates by
`src`, so reloading or SPA-navigating won't double-load.

## The loader.js Endpoint

The `src` URL almost always points at an endpoint inside your app — so the
script body can read the merchant's saved settings and bake them in. The
endpoint must respond with `Content-Type: application/javascript`.

### Recommended response headers

```js theme={null}
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
res.setHeader("Cache-Control", "public, max-age=60");
res.setHeader("Access-Control-Allow-Origin", "*");
```

The 60-second cache balances freshness with throughput. If the merchant
disables the app in admin, the storefront picks up the new disabled state
within a minute.

### Disabled / unconfigured state

Return a no-op comment when the merchant hasn't enabled the app yet — the
loader still appends a `<script>` tag, but it does nothing.

```js theme={null}
if (!cfg.enabled) {
  return res.status(200).send(`/* my-app: disabled for ${escapeJs(slug)} */`);
}
```

Don't return 4xx — the loader treats non-200 as an error and you lose the
ability to debug from a browser request.

### Resolving the merchant slug

The loader passes `domainSlug` as a query string, but you should also
accept the `x-domain-slug` header and the request `Host`:

```js theme={null}
const slug =
  req.query.domainSlug ||
  req.headers["x-domain-slug"] ||
  (req.headers.host || "").split(".")[0] ||
  "";
```

## Minimal Example

A complete script-type app has two parts — the listing bundle and the
HTTP endpoints your app serves:

```
Listing bundle:
├── app.json
├── icon.svg
└── admin/app-home.html

Endpoints served by your app:
├── /config      # GET/POST merchant config
├── /loader      # Returns the storefront IIFE
└── /flags       # GET — per-product flags read by the loader
```

### `app.json`

```json theme={null}
{
  "id": "early-access",
  "handle": "early-access",
  "name": "Early Access — Pre-orders",
  "vendor": "LaunchMyStore",
  "version": "1.0.0",
  "summary": "Turn any product into a pre-order — no theme edits.",
  "categories": ["marketing-and-conversion"],
  "icon": "https://cdn.simpleicons.org/rocket/F59E0B",
  "adminHome": "admin/app-home.html",
  "extensions": {
    "appScripts": [
      {
        "id": "early-access-loader",
        "src": "/api/apps/early-access/loader",
        "loadStrategy": "defer",
        "position": "head"
      }
    ]
  }
}
```

### `loader.js`

The loader returns an IIFE that runs on the storefront. Bake the merchant
slug and any safe config values into the response so the script doesn't
need to make a second round trip just to know which store it's on.

```js theme={null}
import { readConfig } from "./config";

function escapeJs(s) {
  return String(s || "").replace(/[\\'"<>]/g, (c) => ({
    "\\": "\\\\", "'": "\\'", '"': '\\"', "<": "\\u003c", ">": "\\u003e",
  })[c]);
}

export default function handler(req, res) {
  res.setHeader("Content-Type", "application/javascript; charset=utf-8");
  res.setHeader("Cache-Control", "public, max-age=60");
  res.setHeader("Access-Control-Allow-Origin", "*");

  const slug =
    req.query.domainSlug ||
    req.headers["x-domain-slug"] ||
    (req.headers.host || "").split(".")[0] ||
    "";
  if (!slug) return res.status(200).send("/* early-access: no slug */");

  const cfg = readConfig(slug);
  if (!cfg.enabled) {
    return res.status(200).send(`/* early-access: disabled */`);
  }

  const slugJs = escapeJs(slug);

  return res.status(200).send(`/*! early-access for ${slugJs} */
(function(){
  if (window.__earlyAccess) return;
  window.__earlyAccess = { installed: true };

  function start() {
    var path = location.pathname || '';
    var m = path.match(/\\/products\\/([^/?#]+)/);
    if (!m) return;
    var handle = decodeURIComponent(m[1]);

    fetch('/api/apps/early-access/flags?domainSlug=${slugJs}')
      .then(function(r){ return r.ok ? r.json() : null; })
      .then(function(j){
        if (!j || !j.enabled || !j.flags || !j.flags[handle]) return;
        // mutate DOM here — relabel Add-to-cart, append a badge, etc.
      })
      .catch(function(){});
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', start, { once: true });
  } else {
    start();
  }
})();`);
}
```

### `config.js`

Stores the merchant's settings keyed by store slug. Keep it simple:
`readConfig(slug)` / `writeConfig(slug, cfg)` with the slug normalised
(never trust it as a path segment) and a short in-memory cache in front
of your store.

## Verifying The Loader Is Wired

Two HTTP calls confirm everything is connected end-to-end:

```bash theme={null}
# 1. Your loader returns valid JS for this merchant
curl 'https://acme-store.launchmystore.io/api/apps/early-access/loader?domainSlug=acme-store' \
  -H 'Accept: application/javascript'

# 2. /api/apps/extensions surfaces it in scripts[]
curl 'https://acme-store.launchmystore.io/api/apps/extensions?domainSlug=acme-store' \
  | jq '.scripts[] | select(.appHandle == "early-access")'
```

The second call should return an entry like:

```json theme={null}
{
  "appId": "early-access",
  "appHandle": "early-access",
  "id": "app-script-early-access-loader",
  "src": "/api/apps/early-access/loader",
  "loadStrategy": "defer",
  "position": "head",
  "config": {}
}
```

If `scripts[]` doesn't contain your entry, the local manifest cache may be
stale — touch the `app.json` or wait 60s.

## CSP & Cross-Origin Notes

The loader injects `<script src>` tags directly into the storefront page,
so the merchant's storefront origin must allow the script's origin. For
scripts served by your own host (relative paths under `/api/apps/…`) this
is automatic. For absolute external URLs, the merchant's CSP — if any —
must list the script host in `script-src`.

If you're injecting a third-party vendor snippet (e.g. Tawk, Clarity,
Hotjar), it's safer to have your loader return a stub that
*appends the vendor script* itself rather than redirecting to the vendor
URL — that way you control caching and can disable the integration without
the merchant changing their CSP.

## Canonical Examples

Several first-party apps in the LaunchMyStore catalog use this pattern and
make a good reference:

* **Channels Hub** — 4-in-1 chat aggregator (Tawk + Intercom + Crisp +
  WhatsApp). Loader emits each vendor snippet only when its credential is
  set.
* **Smart Pixel Manager** — Loads GA4 + Meta + TikTok with consent gating
  wired through a storefront cookie.
* **Early Access** — Per-product pre-order labels.
* **Restock Alerts** — In-page "Notify me" form on sold-out variants.
* **Cobalt Search** — Search typeahead overlay attached to the theme's
  search input.

## See Also

* [Extensions Overview](/extensions/overview) — full manifest reference.
* [Storefront Blocks](/extensions/theme-blocks) — Liquid blocks merchants
  place inside theme sections (different surface, different lifecycle).
* [Web Pixels](/extensions/web-pixel) — sandboxed customer-event scripts
  for `page_viewed`, `cart_updated`, `checkout_completed`.
* [App Bridge Overview](/app-bridge/overview) — the postMessage contract
  used by admin/checkout iframes (app scripts run in-page and don't need
  App Bridge).
