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 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 caseWhy 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 pagesThe 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)
                                &timestamp=…&signature=…
                                                       ◄──
                          ◄──  7. Stream body back
                               (Liquid responses are
                                rendered through the
                                theme first)
  1. The buyer’s browser hits /apps/{your-handle}/{anything} on the merchant’s domain.
  2. 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.
  3. The platform signs the outbound query with HMAC-SHA256 using your app’s clientSecret and appends four params: shop, path_prefix, timestamp, signature.
  4. Your server verifies the signature and runs the request.
  5. 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."
    }
  }
}
FieldRequiredDescription
urlyesThe 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).
subpathyesThe path segment after the prefix. With subpath: "shiprocket", requests are matched at /apps/shiprocket/*. Conventionally matches your app handle.
subpath_prefixyesAlways apps. Reserved for future expansion (e.g. tools).
descriptionnoFree-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 URLhttps://raja337276.launchmystore.io/apps/shiprocket/api/tracking?awb=ABC123
Your server receivesPOST/GET https://shiprocket.your-domain.app/api/tracking?awb=ABC123&shop=raja337276&path_prefix=/apps/shiprocket&timestamp=…&signature=…
HTTP methodPreserved verbatim — GET stays GET, POST stays POST.
BodyForwarded verbatim with the original Content-Type.
Headers addedX-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):
ParamValue
shopThe merchant’s domainSlug (e.g. raja337276). Stable per store.
path_prefix/apps/{your-subpath} (e.g. /apps/shiprocket).
timestampUnix epoch seconds (string). Reject requests older than 5 minutes.
signatureHex-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:
  1. Take every query param except signature.
  2. Sort the keys alphabetically (ASCII).
  3. Join as key=value with no separator (no &, no ,).
  4. 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 objectsshop, customer, cart, settings, linklists, etc. No need to pass them through; they’re already in scope.

Common patterns

1. Order tracking widget

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

LimitValue
Request body size5 MB
Response body sizeUnlimited (streamed)
Upstream timeout30 s
Concurrency per merchantNo platform cap — your server is the bottleneck
Allowed methodsGET, 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.