> ## 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

> Serve dynamic content from your app server on the merchant's own domain

# 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](/extensions/theme-blocks) (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](/extensions/app-scripts) instead. If you need to render UI
*inside* checkout, use [Checkout UI](/extensions/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`:

```json theme={null}
{
  "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.                                                                        |

<Note>
  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`).
</Note>

## 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&timestamp=…&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:

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=acme-storetimestamp=1716700000
```

And the signature is:

```
signature = hex(HMAC-SHA256(canonical, your_app.clientSecret))
```

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:

```js theme={null}
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:

```js theme={null}
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:

```js theme={null}
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:

```js theme={null}
app.get('/api/tracking', requireProxy, async (req, res) => {
  const data = await fetchCarrierStatus(req.query.awb);
  res.json(data);
});
```

```js theme={null}
// 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](/aqua/objects),
[filter](/aqua/filters), or [tag](/aqua/tags):

```js theme={null}
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.

<Note>
  `application/liquid` responses are rendered with the storefront's full
  [global objects](/aqua/objects) — `shop`, `customer`, `cart`,
  `settings`, `linklists`, etc. No need to pass them through; they're
  already in scope.
</Note>

## Common patterns

### 1. Order tracking widget

A [storefront snippet](/extensions/storefront-snippets) on `/orders/{id}`
calls your proxy endpoint:

```liquid theme={null}
{% 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:

```http theme={null}
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.

```jsonc theme={null}
// 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()`:

```js theme={null}
import { signAppProxyUrl } from '@launchmystore/apps-shared';

const url = signAppProxyUrl({
  baseUrl:      'http://localhost:4101',
  subPath:      '/api/tracking',
  extraQuery:   { awb: 'ABC123' },
  shop:         'acme-store',
  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](/extensions/storefront-snippets) — call your
  proxy endpoints from theme-author Liquid.
* [App Scripts](/extensions/app-scripts) — when you only need a `<script>`
  tag on every page (no signed server endpoint).
* [Webhooks](/api-reference/webhooks/overview) — 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](/app-bridge/session-tokens) — equivalent
  trust mechanism for **admin** iframes.
