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
| Field | Required | Type | Description |
|---|
network_access | no | boolean | Opt-in flag. Defaults to false (sandboxed, no network). |
allowed_hosts | yes when network_access is true | string[] | 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:
| Manifest | Outcome |
|---|
"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
| Limit | Value | Notes |
|---|
| Response body size | 64 KB | Excess is truncated. The function gets only the first 64 KB. |
| Total time per call | 1500 ms | Includes DNS + TCP + TLS + read. After timeout, the call fails with -4. |
| Allowed schemes | https only | http:// is rejected at runtime even if the host is in allowed_hosts. |
| Concurrent calls | 1 | Calls are serialized within a single function invocation. |
| Calls per invocation | 4 | After 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:
| Code | Meaning | Action |
|---|
-1 | network_access not enabled on this function. | Manifest bug — set network_access: true and reinstall. |
-2 | URL was malformed (couldn’t parse, invalid scheme, etc.). | Fix the URL your function builds. |
-3 | Host not in allowed_hosts. | Add the host to the manifest’s allowed_hosts list. |
-4 | Request timed out (>1500 ms) or transport error (DNS, TLS, connection refused). | Make the upstream faster or fail open. |
-5 | Per-invocation call cap (4) reached. | Reduce the number of outbound calls. |
>= 0 | Number 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:
- 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.
- 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