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.
App Proxy 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
- 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:
| 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://acme-store.launchmystore.io/apps/shiprocket/api/tracking?awb=ABC123:
| Value |
|---|
| Merchant storefront URL | https://acme-store.launchmystore.io/apps/shiprocket/api/tracking?awb=ABC123 |
| Your server receives | POST/GET https://shiprocket.your-domain.app/api/tracking?awb=ABC123&shop=acme-store&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 signature query param is authoritative). |
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. acme-store). 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:
And the signature is:
This is the canonical-string HMAC rule the platform uses to sign every
App Proxy request — alphabetically-sorted key=value pairs joined with
no separator, hashed with HMAC-SHA256 under your app’s clientSecret.
Verifying in your app
If your app uses the @launchmystore/apps-shared package, drop in the
ready-made middleware:
Or verify by hand
Any HTTP framework / language. Pseudo-code:
Always pair this with a freshness check:
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:
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:
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:
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:
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.
For unit tests, the shared package exports signAppProxyUrl():
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.