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 Proxy
An App Proxy lets your app serve dynamic content — tracking widgets,
account dashboards, lookup forms, AJAX endpoints — from the merchant’s
own storefront domain. A buyer requests
https://store-name.launchmystore.io/apps/{your-app}/api/tracking?awb=…,
the platform signs and forwards the request to your server, and your
response is streamed back as if it came from the merchant’s domain.
This is the equivalent of Shopify’s App Proxy and lets you ship customer-
facing surfaces that:
- Read first-party cookies (cart, session, locale) — no CORS, no
third-party cookie blocking.
- Render Aqua templates with full theme context (
shop, customer,
cart, theme settings) so your app blends visually with the store.
- Look authentic to buyers — no random
*.your-domain.com iframe.
When to use App Proxy
| Use case | Why App Proxy |
|---|
Shipment tracking widget on /orders/{id} | Buyers see your status pulled live from the carrier, on the merchant’s domain. |
Customer-facing wishlist / loyalty page (/apps/loyalty/dashboard) | First-party cookies + theme styling, no iframe friction. |
AJAX endpoints called from a storefront block (e.g. fetch('/apps/reviews/api/list?productId=…')) | Same-origin → no CORS preflight, sessions just work. |
| Server-rendered Aqua pages | The platform renders your application/liquid response through the theme engine. |
If you only need a static script tag, use
App Scripts instead. If you need to render UI
inside checkout, use Checkout UI.
How it works
Buyer browser Storefront Your app server
───────────── ────────── ───────────────
GET 1. Match /apps/{handle}/* 4. Verify signature
/apps/shiprocket/api/ from query
tracking?awb=ABC123 ───► 2. Look up your appProxy ──► 5. Run handler
manifest (URL + secret) 6. Respond
3. Forward + sign: (JSON or
?shop=…&path_prefix=… application/liquid)
×tamp=…&signature=…
◄──
◄── 7. Stream body back
(Liquid responses are
rendered through the
theme first)
- The buyer’s browser hits
/apps/{your-handle}/{anything} on the
merchant’s domain.
- The storefront’s middleware rewrites the URL to the internal proxy
handler and looks up your manifest’s
extensions.appProxy block to
find the upstream URL.
- The platform signs the outbound query with HMAC-SHA256 using your
app’s
clientSecret and appends four params: shop, path_prefix,
timestamp, signature.
- Your server verifies the signature and runs the request.
- The response streams back to the buyer. If your response’s
Content-Type is application/liquid, the platform first renders it
through the merchant’s active theme with global objects (shop,
customer, cart, etc.).
Manifest
Declare a proxy in app.json under extensions.appProxy:
{
"handle": "shiprocket",
"name": "Shiprocket",
"version": "1.0.0",
"extensions": {
"appProxy": {
"url": "https://shiprocket.your-domain.app",
"subpath": "shiprocket",
"subpath_prefix": "apps",
"description": "Storefront proxy for tracking + AWB lookup."
}
}
}
| Field | Required | Description |
|---|
url | yes | The base URL of your app server. The platform appends subPath and signed params to this. May include a path prefix (e.g. https://api.acme.app/v1). |
subpath | yes | The path segment after the prefix. With subpath: "shiprocket", requests are matched at /apps/shiprocket/*. Conventionally matches your app handle. |
subpath_prefix | yes | Always apps. Reserved for future expansion (e.g. tools). |
description | no | Free-text shown in the admin app-detail page so merchants know what the proxy does. |
Only one App Proxy per app is supported. If you need to serve multiple
namespaces (tracking, returns, lookup), route them under sub-paths of the
same url (e.g. /api/tracking, /api/returns).
URL mapping
With the manifest above and a buyer request to
https://raja337276.launchmystore.io/apps/shiprocket/api/tracking?awb=ABC123:
| Value |
|---|
| Merchant storefront URL | https://raja337276.launchmystore.io/apps/shiprocket/api/tracking?awb=ABC123 |
| Your server receives | POST/GET https://shiprocket.your-domain.app/api/tracking?awb=ABC123&shop=raja337276&path_prefix=/apps/shiprocket×tamp=…&signature=… |
| HTTP method | Preserved verbatim — GET stays GET, POST stays POST. |
| Body | Forwarded verbatim with the original Content-Type. |
| Headers added | X-LMS-Shop-Domain, X-LMS-App-Handle, X-LMS-Hmac (body HMAC), X-Shopify-Hmac-SHA256 (canonical-string HMAC). |
The path you read on your server is subPath only — the
/apps/{handle} prefix is consumed by the platform.
Signature (HMAC-SHA256)
Every proxied request is signed so your server can trust the shop value
and reject anyone calling your URL directly.
Signed params (always forwarded):
| Param | Value |
|---|
shop | The merchant’s domainSlug (e.g. raja337276). Stable per store. |
path_prefix | /apps/{your-subpath} (e.g. /apps/shiprocket). |
timestamp | Unix epoch seconds (string). Reject requests older than 5 minutes. |
signature | Hex-encoded HMAC-SHA256. The thing you verify. |
Any caller-supplied query param (e.g. ?awb=ABC123) is also included
in the signature, so a tampered URL fails verification.
Canonical string
The signature is computed over a canonical string built by:
- Take every query param except
signature.
- Sort the keys alphabetically (ASCII).
- Join as
key=value with no separator (no &, no ,).
- Array values are joined by
, before signing.
For the URL above, the canonical string is:
awb=ABC123path_prefix=/apps/shiprocketshop=raja337276timestamp=1716700000
And the signature is:
signature = hex(HMAC-SHA256(canonical, your_app.clientSecret))
This matches the Shopify App Proxy signing rule exactly — existing
verifier libraries written for Shopify work here too.
Verifying in your app
If your app uses the @launchmystore/apps-shared package, drop in the
ready-made middleware:
import express from 'express';
import { requireAppProxySignature } from '@launchmystore/apps-shared';
const app = express();
const requireProxy = requireAppProxySignature({
clientSecret: process.env.CLIENT_SECRET,
// Reject timestamps older than 5 minutes (default).
maxSkewSeconds: 300,
// Set true in dev so unsigned curl requests still go through.
// ALWAYS false in production.
optional: process.env.NODE_ENV !== 'production',
});
app.get('/api/tracking', requireProxy, (req, res) => {
// Verified context attached by the middleware:
// req.appProxy = { shop, pathPrefix, timestamp, verified }
const shop = req.appProxy.shop;
const awb = req.query.awb;
// ...look up the merchant's carrier creds by `shop`, call the carrier...
res.json({ status: 'In Transit', shop, awb });
});
Or verify by hand
Any HTTP framework / language. Pseudo-code:
import crypto from 'node:crypto';
function verify(query, clientSecret) {
const { signature, ...rest } = query;
if (!signature) return false;
const canonical = Object.keys(rest)
.sort()
.map((k) => {
const v = Array.isArray(rest[k]) ? rest[k].join(',') : String(rest[k]);
return `${k}=${v}`;
})
.join('');
const expected = crypto.createHmac('sha256', clientSecret)
.update(canonical)
.digest('hex');
// Constant-time compare
return signature.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
}
Always pair this with a freshness check:
const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(query.timestamp));
if (skew > 300) return false; // 5-minute window
Response types
Your app server can respond with either a regular content type or
application/liquid for server-rendered theme HTML.
JSON / HTML / text — passthrough
The body streams back to the browser verbatim. Use this for AJAX endpoints
called from your storefront block:
app.get('/api/tracking', requireProxy, async (req, res) => {
const data = await fetchCarrierStatus(req.query.awb);
res.json(data);
});
// From a storefront block's JS:
fetch('/apps/shiprocket/api/tracking?awb=' + awb)
.then(r => r.json())
.then(renderStatus);
Same-origin — no CORS, no preflight, sends first-party cookies.
application/liquid — server-rendered theme HTML
Respond with Content-Type: application/liquid and the platform renders
your body through the merchant’s active theme before sending it to the
browser. Your response can use any Aqua object,
filter, or tag:
app.get('/dashboard', requireProxy, (req, res) => {
res.setHeader('Content-Type', 'application/liquid');
res.send(`
{% layout 'theme' %}
<main class="page-wrap">
<h1>Welcome back, {{ customer.first_name | default: 'friend' }}</h1>
<p>Your loyalty points: <strong>{{ customer.metafields.app_loyalty.points }}</strong></p>
</main>
`);
});
The buyer sees a fully themed page at
https://store.launchmystore.io/apps/loyalty/dashboard that inherits the
merchant’s header, footer, fonts, and colours.
application/liquid responses are rendered with the storefront’s full
global objects — shop, customer, cart,
settings, linklists, etc. No need to pass them through; they’re
already in scope.
Common patterns
A storefront snippet on /orders/{id}
calls your proxy endpoint:
{% comment %} snippets/shiprocket-tracking.aqua {% endcomment %}
<div id="shiprocket-tracking" data-awb="{{ order.metafields.app_shiprocket.awb }}">
Loading shipment status...
</div>
<script>
(function() {
var el = document.getElementById('shiprocket-tracking');
var awb = el.dataset.awb;
if (!awb) return;
fetch('/apps/shiprocket/api/tracking?awb=' + encodeURIComponent(awb))
.then(function(r) { return r.json(); })
.then(function(d) {
el.innerHTML = '<strong>' + d.status + '</strong><br>' + d.last_scan;
});
})();
</script>
2. Customer-facing app page
A merchant-link in the storefront header points to
/apps/loyalty/dashboard. Your proxy returns application/liquid and
the platform themes it.
3. Same-origin AJAX from a storefront block
A reviews app’s block JS posts new reviews to
/apps/reviews/api/submit. Cookies sent automatically — your server
identifies the customer via req.appProxy.shop + the platform session
cookie.
Caching
The platform forwards your response’s Cache-Control header. To cache
proxied responses at the edge, set:
Cache-Control: public, max-age=60, s-maxage=300
JSON responses with Cache-Control: no-store (the default) are not
cached.
Security checklist
- ✅ Always verify the signature in production. The
optional: true
dev mode skips verification — never ship that to prod.
- ✅ Check
timestamp freshness (≤ 5 minutes). Prevents replay
attacks if a logged URL leaks.
- ✅ Use
req.appProxy.shop (verified) — never trust ?shop= from a
caller without verification.
- ✅ Don’t echo unsanitized user input into
application/liquid
responses. Liquid output is auto-escaped, but {{ x }} inside an
attribute can still break if x contains quotes. Use the escape
filter for paranoia.
- ❌ Don’t expose write endpoints (anything that mutates merchant
data) on the proxy unless you also require a signed-in customer session
— the proxy only proves the request originated from a merchant
storefront, not which buyer made it.
Development & testing
For local development, your app server typically runs on
http://localhost:PORT. Point your dev app’s extensions.appProxy.url
at it via a tunneling tool (ngrok, cloudflared, tailscale funnel) so the
platform can reach it from the cloud.
// app.json — dev variant
"extensions": {
"appProxy": {
"url": "https://your-tunnel.ngrok.app",
"subpath": "shiprocket",
"subpath_prefix": "apps"
}
}
For unit tests, the shared package exports signAppProxyUrl():
import { signAppProxyUrl } from '@launchmystore/apps-shared';
const url = signAppProxyUrl({
baseUrl: 'http://localhost:4101',
subPath: '/api/tracking',
extraQuery: { awb: 'ABC123' },
shop: 'raja337276',
pathPrefix: '/apps/shiprocket',
clientSecret: process.env.CLIENT_SECRET,
});
const res = await fetch(url);
// → exercises the same canonical-string + HMAC path the platform uses.
Limits
| Limit | Value |
|---|
| Request body size | 5 MB |
| Response body size | Unlimited (streamed) |
| Upstream timeout | 30 s |
| Concurrency per merchant | No platform cap — your server is the bottleneck |
| Allowed methods | GET, POST, PUT, PATCH, DELETE |
A timeout or 5xx from your server returns 502 Bad Gateway to the buyer.
The buyer’s browser sees that response; the storefront does not retry
on your behalf — handle retries in your app.
See also
- Storefront Snippets — call your
proxy endpoints from theme-author Liquid.
- App Scripts — when you only need a
<script>
tag on every page (no signed server endpoint).
- Webhooks — server-to-server events
from the platform to your app (the inbound direction; App Proxy is the
buyer-to-app direction).
- App Bridge: Session Tokens — equivalent
trust mechanism for admin iframes.