# Proxy with Express

> Serve the MetricRaven script and events from an Express server on Node.js.

Source: https://metricraven.com/docs/proxy-express  
Markdown: https://metricraven.com/docs/proxy-express.md  
Section: Proxy

Two Express routes relay the script and events, using Node's built-in fetch.

## Set it up

1. **Add the routes**

   Add the two routes to your existing app, before any catch-all route. Node.js 18 or later is required for `fetch`.

   ```javascript title="metricraven-proxy.js"
   // metricraven-proxy.js · Express 4 or 5 on Node.js 18+
   import express from "express";

   const app = express();
   // Behind a load balancer or CDN? Trust it so req.ip is the visitor, not the balancer.
   // app.set("trust proxy", 1);

   async function relay(req, res, url, cacheControl) {
     const headers = { "x-visitor-ip": req.ip.replace(/^::ffff:/, "") };
     for (const name of ["content-type", "origin", "user-agent"]) {
       if (req.headers[name]) headers[name] = req.headers[name];
     }
     try {
       const upstream = await fetch(url, {
         method: req.method,
         headers,
         body: req.method === "POST" ? req.body : undefined,
         signal: AbortSignal.timeout(15000),
       });
       res
         .status(upstream.status)
         .set("Content-Type", upstream.headers.get("content-type") || "application/octet-stream")
         .set("Cache-Control", cacheControl)
         .send(Buffer.from(await upstream.arrayBuffer()));
     } catch {
       res.status(502).end();
     }
   }

   app.get("/a/script", (req, res) =>
     relay(req, res, "https://metricraven.com/js/script.js", "public, max-age=3600"),
   );
   // The tracker sends JSON as text/plain; keep the raw body untouched.
   app.post("/a/event", express.raw({ type: () => true, limit: "64kb" }), (req, res) =>
     relay(req, res, "https://metricraven.com/api/v1/collect", "no-store"),
   );

   // Mount these routes in your existing app, or run this file on its own:
   app.listen(process.env.PORT || 3000);
   ```

2. **Set trust proxy if you're behind a load balancer**

   `req.ip` is the visitor only when Express knows which proxies to trust. On Heroku, Render, Railway or behind Nginx, uncomment `app.set("trust proxy", 1)` (one hop).

3. **Point your script tag at your own paths**

   Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too.

   ```html title="Your website's shared layout"
   <script defer src="/a/script" data-api="/a/event" data-site-id="YOUR_SITE_ID"></script>
   ```

> **Verify it works**
>
> Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser.
>
> In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded.

## Notes

- If a CDN or load balancer sits in front of this server, the connecting address is that proxy, not the visitor. Use the visitor IP header it sets (and only one it overwrites, so visitors can't fake it).
- The event route uses `express.raw()` so the body is relayed byte for byte. A global `express.json()` doesn't touch it, because the tracker sends `text/plain`.

How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy).

## Details

- The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server.
- `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`.
- MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations.
- A proxy doesn't guarantee that every blocker will allow tracking.
