A small Node service and a Dockerfile for any platform that runs containers: Docker, Kubernetes, or anything behind HAProxy, Envoy or a cloud load balancer that can route one path to a container. Your front proxy routes the path the dashboard issued to it, on your own host, and it forwards only the browser SDK's submissions to Fingerly. It is not an open proxy: the upstream is a constant in the code, and every other path and method is refused. Version 1.0.0.
The Traefik and Google Cloud Run templates run this same service.
Set up
Step 1: Create the proxy
In the dashboard, open Integration > Proxy keys and choose New proxy. Pick the environment of the public key it will forward, and Container under How it runs. Copy the proxy key it shows once, and note the path it issued, such as
/k7x2qm4a.The dashboard then prints the steps below, filled in with your proxy's own path and key. They stay under Show setup in the proxy's menu, without the key, which is never shown again.
Step 2: Save the service
Save
server.mjsin an empty folder.server.mjsis a small Node service with no dependencies: the built-in HTTP server andfetch. It readsFINGERLY_PROXY_KEY,FINGERLY_PATH,FINGERLY_CLIENT_IP_SOURCEandPORT(default8080), and refuses to start when the address source is not one it knows. The dashboard prints it; copy it from there.Step 3: Save its Dockerfile
Beside
server.mjs. The image has no dependencies to install and runs as an unprivileged user, with a numeric ID so Kubernetes'runAsNonRootcan check it.FROM node:24-alpine WORKDIR /app COPY server.mjs ./ ENV NODE_ENV=production PORT=8080 USER 1000:1000 EXPOSE 8080 CMD ["node", "server.mjs"]Step 4: Build and run it
Take the key from your secret store, never a file in version control.
remoteis right only when visitors connect to the container directly; behind a proxy, choose the address source that fits.docker build -t site-relay . export FINGERLY_PROXY_KEY='fly_px_production_…' # from your secret store docker run -d --name site-relay --restart unless-stopped -p 8080:8080 \ -e FINGERLY_PATH=/k7x2qm4a -e FINGERLY_PROXY_KEY \ -e FINGERLY_CLIENT_IP_SOURCE=remote \ site-relayStep 5: Route the path to it
From the proxy or load balancer in front, send exactly this path on your site's host to the container,
POSTbodies included.POST https://www.example.com/k7x2qm4a -> http://site-relay:8080/k7x2qm4aStep 6: Point the browser SDK at the path
import { load } from '@fingerly/web-js' const fingerly = await load({ apiKey: 'fly_pk_production_…', endpoints: '/k7x2qm4a', })The SDK posts both of its calls to the path exactly as given. Once pages use it, the proxy's row in Proxy keys shows it last seen, running
container/1.0.0, with its requests over the last 30 days.
How the visitor's address is trusted
FINGERLY_CLIENT_IP_SOURCE says where the visitor's address comes from. The service sends it as x-fingerly-client-ip, and refuses to start when the value is not one of these:
| Value | Reads | Safe when |
|---|---|---|
remote (default) | The address of the connection. | Visitors connect to the container directly. |
header:<name> | One header, such as header:x-real-ip. | The proxy in front sets that header on every request, overwriting what the visitor sent. |
xff:<n> | The n-th X-Forwarded-For entry from the right. | Your own proxies append exactly n-1 entries after the visitor's address. |
Only a header the front proxy overwrites is safe. A header it merely passes along, or the left-most X-Forwarded-For entry, is whatever the visitor chose to send. Behind one proxy that appends the connection address to X-Forwarded-For, xff:1 is right; behind a load balancer and then a proxy that each append one, xff:2.
A repeated header, or a value that is not an IP address, is sent as empty, and the API then refuses the call with 422 and invalid_visitor_metadata rather than guessing.
What it sends and relays
Upstream, to https://api.fingerly.io/api/v1/ingest and nowhere else, on headers built from scratch: content-type, x-api-key, Idempotency-Key when present, and the x-fingerly-* headers the proxy sets itself (the proxy key, the visitor's address, the page origin, the user agent and x-fingerly-proxy-version: container/1.0.0). Cookies, Authorization and any x-fingerly-* header a visitor sends never leave.
Other methods answer 405 with Allow: POST, and other paths 404. Bodies over 1 MiB are refused with 413, redirects are not followed, and a missing or malformed key answers 503 rather than forwarding anything. With FINGERLY_PATH unset, the service answers 404 to everything.
Back to the page: the API's status and body, with only content-type, x-request-id, ratelimit-limit, ratelimit-remaining and retry-after, plus cache-control: no-store. Nothing relayed names Fingerly.