Skip to main content

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

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. For sandboxed customer-event listeners, use Web Pixels.

When to Use App Scripts

NeedUse
Floating chat / WhatsApp / Calendly button on every pageApp script
GA4 / Meta Pixel / TikTok / Hotjar / ClarityApp script
Storefront-wide search typeahead, points pill, pre-order badgeApp script
Liquid block the merchant places inside a sectionStorefront Block
Sandboxed listener for page_viewed, cart_updated, etc.Web Pixel
<head> / </body> overlay the merchant can toggle per-themeStorefront Embed
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 wired in src/pages/api/themes/render-page.js and surfaces local manifests via _localManifests.js::getLocalAppScripts(). You don’t need to invoke either — declaring appScripts in app.json is enough.

Manifest

Declare app scripts under extensions.appScripts in your app.json:
{
  "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

FieldTypeDefaultDescription
idstringUnique identifier within your app. Surfaced in the loader response and on the injected <script data-app-id="…"> tag for easy debugging.
srcstringURL 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.
appIdstring<appHandle>Optional override surfaced on the injected <script data-app-id> attribute.
configobject{}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:
{
  "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.
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.
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:
const slug =
  req.query.domainSlug ||
  req.headers["x-domain-slug"] ||
  (req.headers.host || "").split(".")[0] ||
  "";

Minimal Example

A complete script-type app has five files:
public/marketplace-apps/apps/early-access/
├── app.json
├── icon.svg
└── admin/app-home.html

src/pages/api/apps/early-access/
├── config.js     # GET/POST merchant config (file-backed)
├── loader.js     # Returns the storefront IIFE
└── flags.js      # GET — per-product flags read by the loader

app.json

{
  "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.
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

File-backed merchant config under data/<handle>/<safeSlug>.config.json. See Channels Hub for the canonical template — readConfig(slug) / writeConfig(slug, cfg) with the slug normalised through path.basename and a 60s in-memory cache.

Verifying The Loader Is Wired

Two HTTP calls confirm everything is connected end-to-end:
# 1. Your loader returns valid JS for this merchant
curl 'http://raja337276.localhost:3000/api/apps/early-access/loader?domainSlug=raja337276' \
  -H 'Accept: application/javascript'

# 2. /api/apps/extensions surfaces it in scripts[]
curl 'http://raja337276.localhost:3000/api/apps/extensions?domainSlug=raja337276' \
  | jq '.scripts[] | select(.appHandle == "early-access")'
The second call should return an entry like:
{
  "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 CustomerLMS 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 — full manifest reference.
  • Storefront Blocks — Liquid blocks merchants place inside theme sections (different surface, different lifecycle).
  • Web Pixels — sandboxed customer-event scripts for page_viewed, cart_updated, checkout_completed.
  • App Bridge Overview — the postMessage contract used by admin/checkout iframes (app scripts run in-page and don’t need App Bridge).