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.

Network access

Functions are sandboxed by default — no network, no filesystem, no ambient time. Opt in to outbound HTTP per-function by setting network_access: true on the manifest and listing the hosts you need in allowed_hosts.
Status — partial wiring. Manifest validation, the host allowlist enforcement, outbound-call telemetry, and the lms_host.fetch_url host import are live. The JS-side runtime shim that exposes this import as globalThis.fetch from user code is not yet wired in the current dynamic-mode (Javy -d) build — a function that calls globalThis.fetch(...) will throw 'fetch' is not defined because Javy ships QuickJS without the Fetch API.Until the shim ships, build functions that work without outbound HTTP. The manifest fields documented below are accepted (and enforced server-side) so you can declare intent today; outbound calls will start working once the runtime shim lands without any manifest change.

When to use it

Most functions should not need network access. The platform supplies cart, customer, metafields, locations, shop currency, and addresses inline — that covers 95% of real-world function logic. Reach for network access only when:
  • You need a real-time lookup that the merchant can’t pre-compute (live currency rate, live tax table, stock at a 3PL).
  • You’re integrating with an external service whose data isn’t appropriate to mirror into metafields (fraud score, loyalty tier, carrier rate).
  • You need to side-effect to your own backend (write an analytics event when a function fires — though prefer the host’s audit log when possible).
If you find yourself reaching for network access because the data is “convenient” to fetch live, consider mirroring it into a metafield instead. Function dispatch happens on every cart change and every checkout transition — a slow outbound call shows up as a slow cart.

Manifest

{
  "handle": "loyalty-bonus",
  "name": "Loyalty Bonus",
  "version": "1.0.0",
  "extensions": {
    "functions": [
      {
        "type": "discount",
        "handle": "loyalty-tier-bonus",
        "entrypoint": "dist/loyalty.wasm",
        "network_access": true,
        "allowed_hosts": [
          "api.loyalty.example.com",
          "edge.loyalty.example.com"
        ]
      }
    ]
  }
}
network_access and allowed_hosts apply per function (not per app), so a single app can have one function with network access and others without.

Field reference

FieldRequiredTypeDescription
network_accessnobooleanOpt-in flag. Defaults to false (sandboxed, no network).
allowed_hostsyes when network_access is truestring[]Allowlist of hostnames the function may reach.
allowed_hosts entries are hostnames only — no scheme, no port, no path, no wildcards. The install endpoint rejects malformed entries with HTTP 400:
ManifestOutcome
"api.loyalty.example.com"OK
"loyalty.example.com"OK
"127.0.0.1"Rejected — not a valid public hostname pattern.
"https://api.loyalty.example.com"Rejected — scheme not allowed.
"api.loyalty.example.com/v1"Rejected — path not allowed.
"api.loyalty.example.com:8443"Rejected — port not allowed.
"*.loyalty.example.com"Rejected — wildcards not supported.
"localhost"Rejected — single-label hosts rejected by the validator.
The validator regex requires at least one . and each label to match [a-z0-9]([a-z0-9-]{0,61}[a-z0-9])? (case-insensitive). This is intentional: an allowlist is not a regex — every host you want to reach must be enumerated.

Install-time validation

The CustomerLMS install endpoint (/api/apps/install-extensions) runs the same validator as the dispatcher before persisting the manifest. Errors block install with a clear message:
HTTP/1.1 400 Bad Request
{ "error": "Invalid function manifest for \"loyalty-tier-bonus\": allowed_hosts entry \"https://api.loyalty.example.com\" is not a valid hostname" }
Fix the manifest and re-call install. There is no separate “validate” endpoint — the install path is the contract.

Runtime API

Your WASM module gets a single synchronous host import (exposed by the Javy runtime, not part of the WASI spec):
lms_host.fetch_url(urlPtr, urlLen, optsPtr, optsLen) -> int32
The bundled JS runtime will expose this as globalThis.fetch once the shim lands — call it exactly as you would in Node:
export default async function main(input) {
  const customer = input.customer;
  if (!customer?.id) return { discounts: [] };

  // (Once the runtime shim is wired.)
  const r = await fetch(
    `https://api.loyalty.example.com/customers/${customer.id}/tier`,
  );
  if (!r.ok) return { discounts: [] };

  const { tier } = await r.json();
  if (tier !== 'platinum') return { discounts: [] };

  return {
    discounts: [
      { title: 'Platinum loyalty bonus', value: 15, valueType: 'percentage', target: 'order', targetSelection: 'all' },
    ],
  };
}

Limits

LimitValueNotes
Response body size64 KBExcess is truncated. The function gets only the first 64 KB.
Total time per call1500 msIncludes DNS + TCP + TLS + read. After timeout, the call fails with -4.
Allowed schemeshttps onlyhttp:// is rejected at runtime even if the host is in allowed_hosts.
Concurrent calls1Calls are serialized within a single function invocation.
Calls per invocation4After 4 successful (or failed) calls, subsequent calls fail with -5.
These limits are platform-enforced — your function cannot raise them. If you need a longer call, do the slow work async on your own service and return a cached answer from a fast endpoint.

Return codes

lms_host.fetch_url returns an int32 to your WASM module. The bundled runtime shim translates these to fetch errors / response objects:
CodeMeaningAction
-1network_access not enabled on this function.Manifest bug — set network_access: true and reinstall.
-2URL was malformed (couldn’t parse, invalid scheme, etc.).Fix the URL your function builds.
-3Host not in allowed_hosts.Add the host to the manifest’s allowed_hosts list.
-4Request timed out (>1500 ms) or transport error (DNS, TLS, connection refused).Make the upstream faster or fail open.
-5Per-invocation call cap (4) reached.Reduce the number of outbound calls.
>= 0Number of bytes written to the response buffer.Read the buffer; call succeeded.
-3 is the most common error in practice: a developer copy-pastes a URL with a slightly different host (api.example.com vs api.staging.example.com) and forgets to add the new host to allowed_hosts. The error message logged on the function run row includes the offending host so this is quick to debug.

Dispatch behaviour when a fetch is blocked

When a fetch fails with any of -1 through -5, the function still runs to completion — only the specific fetch_url call returns failure. Your function decides how to react:
const r = await fetch('https://api.loyalty.example.com/tier');
if (!r.ok || r.status >= 500) {
  // Fail open: skip the bonus, don't break the cart.
  return { discounts: [] };
}
If you throw from the function instead of returning, the dispatcher treats it like any other WASM error: the result is discarded (no output applied to the cart / checkout) and an error is logged to the function run row. Other apps’ functions of the same type still run — one bad app cannot take the whole pipeline offline. The platform never auto-retries blocked or timed-out fetches. If you need retries, build them into the function:
async function fetchWithRetry(url, retries = 1) {
  for (let i = 0; i <= retries; i++) {
    const r = await fetch(url);
    if (r.ok) return r;
    if (r.status < 500) return r; // 4xx — don't retry
  }
  return null;
}
…but be mindful of the 1500 ms total-time budget across all calls.

TLS / CORS notes

  • TLS is required. The runtime only allows https:// URLs even if the host is in allowed_hosts. There is no plaintext escape hatch.
  • No CORS. The host import is a server-to-server fetch initiated by the WASM worker process — there is no browser, no preflight, no Access-Control-Allow-Origin to consider. Your endpoint can be locked down to Origin: lms-functions.launchmystore.io if you need to verify the caller.
  • No cookies. The runtime doesn’t carry merchant/customer cookies on outbound calls. Authenticate with an API key or Bearer token your app already has.
  • No streaming. Response bodies are fully buffered (capped at 64 KB) before the function sees them. There is no ReadableStream.

Authenticating outbound calls

The function runtime doesn’t have a built-in secret store. You have two practical options:
  1. Bake a key into the WASM bundle. Simplest for first-party apps — the key sits in your compiled module and lands on the merchant’s disk only as part of your signed bundle. Risk: anyone who pulls the WASM apart can read the key.
  2. Embed the key in a merchant-specific app metafield that the function reads from cart.metafields.app_<handle>.api_key.value. The merchant can rotate the key independently of the function binary.
Option 2 is recommended for production. Future releases will add a proper per-merchant secret API.

Observability

Every outbound call is recorded on the AppFunctionRun row in the meta JSONB column:
{
  "networkAccess": true,
  "outboundCalls": [
    { "host": "api.loyalty.example.com", "status": 200, "bytes": 482, "ms": 134 },
    { "host": "api.loyalty.example.com", "status": 502, "bytes": 0,   "ms": 1502, "errorCode": -4 }
  ]
}
Merchants (in the app management UI) and you (in the developer portal) can audit which hosts each function call actually reached, the status codes, and the latency. This is the first place to look when a function behaves differently in production than in test. For aggregate health, the developer portal surfaces:
  • Calls per function per day — to track usage.
  • p50 / p95 outbound latency — to spot slowness before it shows up as checkout latency.
  • Failure rate by error code — to catch new -3 host-allowlist errors after a deploy.

Common patterns

Real-time rate API

// type: shipping_rate
export default async function main(input) {
  const r = await fetch(
    `https://rates.carrier.example.com/quote?zip=${input.destination.zip}&weight=${cartWeight(input.cart)}`,
    { headers: { 'X-Api-Key': API_KEY } },
  );
  if (!r.ok) {
    // Fail open with a default rate so the cart doesn't break.
    return { rates: [{ name: 'Standard', price: 9.99 }] };
  }
  const { rates } = await r.json();
  return { rates };
}

Geo-IP / fraud check

// type: order_validation
export default async function main(input) {
  const r = await fetch(
    `https://fraud.example.com/score?country=${input.shippingAddress.country}&total=${input.cart.totalPrice}`,
  );
  if (!r.ok) return { errors: [] }; // fail open

  const { score, reason } = await r.json();
  if (score > 0.9) {
    return {
      errors: [
        { message: `Order flagged for review: ${reason}`, target: 'cart' },
      ],
    };
  }
  return { errors: [] };
}

Loyalty tier lookup

// type: discount
export default async function main(input) {
  const id = input.customer?.id;
  if (!id) return { discounts: [] };

  const r = await fetch(
    `https://api.loyalty.example.com/customers/${id}/tier`,
    { headers: { 'Authorization': `Bearer ${API_KEY}` } },
  );
  if (!r.ok) return { discounts: [] };

  const { tier } = await r.json();
  const rate = { gold: 5, platinum: 10, diamond: 15 }[tier] || 0;
  if (rate === 0) return { discounts: [] };

  return {
    discounts: [{
      title: `${tier} loyalty (${rate}%)`,
      value: rate,
      valueType: 'percentage',
      target: 'order',
      targetSelection: 'all',
    }],
  };
}

Security considerations

  • allowed_hosts is an allowlist, not a regex. Every host you want to reach must be enumerated by exact match. This is intentional — a regex allows escalation by typo, an allowlist doesn’t.
  • Don’t include localhost or private IP ranges. The validator rejects single-label hosts and IPs, and the runtime additionally refuses RFC1918 ranges and link-local addresses even if you somehow declared them. The platform does not let WASM functions probe the internal network.
  • Treat your endpoint as untrusted-input territory. Anyone who can install your app on a merchant can trigger calls to your allowed hosts. Rate-limit by API key, log every call, and refuse requests with suspicious payloads.
  • Cache aggressively on your side. Returning a cached response for the same (customer, cart) tuple keeps your function fast and your upstream costs predictable.
  • Rotate API keys regularly. Use the metafield-based pattern (see Authenticating outbound calls) so rotation doesn’t require a WASM rebuild.

Best practices

  • Short timeouts on your side too. Your endpoint should fail in <500 ms 99% of the time. The platform’s 1500 ms hard timeout is a safety net, not a target.
  • Return structured errors. A 200 with { "ok": false, "reason": ... } is faster than a 5xx because the runtime doesn’t have to retry.
  • Cache aggressively. Each verifyCart and addClientOrder can trigger function dispatch — if your endpoint is slow or rate-limited you’ll cause cart latency that shows up in conversion metrics.
  • Keep allow-lists tight. A single misconfigured host can leak data if your function reads sensitive merchandise attributes.
  • Decide fail-open vs fail-closed deliberately. On -4 (timeout), decide whether your function should return a no-op output (fail open — preserve cart usability) or surface an error (fail closed — block the bad state). Most discount/shipping functions should fail open; order-validation and fulfillment-constraints functions might fail closed.
  • Log the request id. Pass the cart.id or customer.id as a header so your upstream logs can be correlated with the function run in the developer portal.

See also