A Node or bun server — the first-party proxy
Most tenants never need this page: the managed proxy covers their usual path. This
guide installs the reference proxy: a small, dependency-free
Node service that puts the whole measurement path on your own domain under
/px/*. It runs on Node or bun; Node is the default documented here. The
install path below — starting the proxy, routing /px/*, the install line, and
the browser round-trip — was proven in an end-to-end run: a real browser, a
strict Content-Security-Policy, and a deliberately unreachable measurement
backend that did not slow the page by a single millisecond.
For collection, privacy, and what the install cannot do to your site, see Start here.
What you get
- The snippet, served from your domain.
GET /px/t.jsreturns the measurement script from a local cache, so no third-party domain ever appears in your pages. - Beacons that stay first-party. The script reports pageviews to
POST /px/eon your domain; the proxy relays them to your ingest endpoint in the background, after it has already answered the browser. - Zero CSP changes. Because both the script and the beacon are same-origin,
a strict policy such as
script-src 'self'andconnect-src 'self'keeps working exactly as it is. You never allowlist an analytics domain. - Verified-origin pageviews. The proxy attaches the visitor’s IP as a signed claim, so your pageview counts can be marked as verified rather than merely claimed. Measurement is cookieless — the proxy sets no cookies and stores nothing about your visitors.
- A service that cannot take your site down. The proxy is fail-open by construction: if the measurement backend is slow, paused, or gone, browsers still get an instant success response and your pages render unchanged. The failure-modes section shows exactly why.
Prerequisites
From onboarding you receive four values (keep the secret in your environment or secret store, never in code or version control):
| Value | Example shape | Used as |
|---|---|---|
| Site key | pk_your_site_key |
data-site in the install line and AURASCOPE_SITE |
| Ingest endpoint | https://ingest.example-analytics.com — your ingest endpoint from onboarding |
AURASCOPE_INGEST_URL |
| Snippet origin | https://snippets.example-analytics.com — your snippet origin from onboarding |
AURASCOPE_SNIPPET_ORIGIN |
| Provenance key ID + secret | key-1 + hex:xxxxxxxx…redacted (obviously-fake placeholder — use the real value you were issued) |
AURASCOPE_PROVENANCE_KEY_ID / AURASCOPE_PROVENANCE_SECRET |
You also need:
- The proxy source, which you receive at onboarding as
aurascope-proxy.tar.gz— one small directory of TypeScript with no runtime dependencies to install. See How you receive the software below, then step 1. - Node 22.6 or newer (or bun). Node runs the TypeScript directly with the
--experimental-strip-typesflag shown below — the flag first shipped in Node 22.6, so on 22.0–22.5 the start command fails with a bad-option error. Our automated checks run this exact start command on Node 22.6 and on current Node on every change, so the minimum above is a tested floor rather than a claim from release notes; if the entrypoint is ever reached on an older runtime it refuses to start and says why, instead of failing obscurely later. - The ability to route two paths on your site’s domain —
GET /px/t.jsandPOST /px/e— through your existing reverse proxy, router, or CDN to a small local service.
How you receive the software
The proxy is a service you run on your own infrastructure, not a library your
application imports — so it is not a package you install from a registry. That is
the normal shape for this piece across the industry: a first-party measurement
proxy is configured into your edge, and its installation is its configuration.
You receive the reference source directly from us at onboarding, as
aurascope-proxy.tar.gz.
There is nothing to verify by hand and nothing to build: unpack it and start it (step 1). The managed proxy is decided and in build for tenants who would rather not simply point a hostname at, and ready-made configuration for common platforms — but the source you receive today is the same code either way.
Install steps
1. Unpack the proxy source, then configure and start it
The archive unpacks into a single directory, aurascope-proxy/, containing
TypeScript sources and nothing to install: there is no build step and no
dependency to fetch. Every command below runs from inside that directory,
because the entrypoint is named by the relative path src/node/main.ts:
tar -xzf aurascope-proxy.tar.gz
cd aurascope-proxy
Put the directory wherever you keep services you run — it is the deployed artifact, not a scratch copy. Then set the environment and start the reference entrypoint:
export AURASCOPE_SITE="pk_your_site_key"
export AURASCOPE_INGEST_URL="https://ingest.example-analytics.com" # your ingest endpoint from onboarding
export AURASCOPE_SNIPPET_ORIGIN="https://snippets.example-analytics.com" # your snippet origin from onboarding
export AURASCOPE_PROVENANCE_KEY_ID="key-1"
export AURASCOPE_PROVENANCE_SECRET="hex:xxxxxxxx…redacted" # from your secret store — never commit it
export AURASCOPE_IP_STRATEGY="x-real-ip" # match your topology; see step 2
node --experimental-strip-types src/node/main.ts
Or under bun: bun src/node/main.ts.
On success it logs one line:
AuraScope first-party proxy listening on 127.0.0.1:8787
The reference deliberately binds to 127.0.0.1 only — nothing on the public
internet can reach it directly; traffic arrives solely through the route you
add in step 2. It answers exactly two routes (GET /px/t.js,
POST /px/e) and returns 404 for everything else; there is no admin surface.
Run it under your usual process supervisor. On SIGINT/SIGTERM it shuts down
gracefully, draining in-flight relays for up to a couple of seconds.
Full configuration reference (the numeric defaults are safe; every numeric setting falls back to its default if the value is invalid):
| Variable | Default | Purpose |
|---|---|---|
AURASCOPE_SNIPPET_ORIGIN |
— (required; the proxy refuses to start without it) | Where the proxy fetches the snippet to cache |
AURASCOPE_INGEST_URL |
a built-in non-tenant default — always set it explicitly | Where beacons are relayed (/v1/events is appended). Caution: startup does not fail if this is unset or mistyped — the proxy still answers 204 with a quiet log while your pageviews never reach your account; only verify step 4 catches it |
AURASCOPE_SITE |
unset | Your pk_ site key; must equal the install line’s data-site |
AURASCOPE_PROVENANCE_KEY_ID |
unset | Key ID for the signed visitor-IP claim |
AURASCOPE_PROVENANCE_SECRET |
unset | Signing secret — raw text, or base64:/hex:-prefixed bytes; environment-only |
AURASCOPE_IP_STRATEGY |
socket |
How the visitor IP is read — see step 2 |
AURASCOPE_BEACON_MAX_BYTES |
65536 |
Hard cap on a beacon body |
AURASCOPE_BODY_DEADLINE_MS |
250 |
Hard deadline for reading a beacon body |
AURASCOPE_RELAY_DEADLINE_MS |
2000 |
Background relay abort deadline |
AURASCOPE_SNIPPET_MAX_AGE_MS |
300000 |
Fresh lifetime of the cached snippet (5 min) |
AURASCOPE_SNIPPET_SWR_MS |
86400000 |
Extra stale-while-revalidate lifetime (24 h) |
AURASCOPE_PORT |
8787 |
Loopback port the proxy listens on |
If the three signing values (AURASCOPE_SITE, key ID, secret) are not all set,
the proxy logs one warning — AuraScope proxy provenance signing is not configured; relaying unsigned — and keeps relaying. Nothing is rejected or
lost; your pageviews are simply kept without the verified-origin mark until
signing is configured. A data-site/AURASCOPE_SITE mismatch is equally
non-destructive: the beacon is kept and flagged, never dropped.
2. Route /px/* on your domain to the proxy
Add two routes to your existing reverse proxy, router, or CDN so the browser sees a single origin — yours:
GET /px/t.js→ the proxyPOST /px/e→ the proxy
Illustrative nginx shape (adapt to whatever fronts your site):
location /px/ {
proxy_pass http://127.0.0.1:8787;
proxy_set_header X-Real-IP $remote_addr;
}
Then set AURASCOPE_IP_STRATEGY to match where the real visitor IP is
trustworthy in your topology — choose it from your network position, never from
a header a visitor could set themselves:
| Your topology | Strategy | Header read |
|---|---|---|
nginx-style reverse proxy setting X-Real-IP |
x-real-ip |
X-Real-IP |
| Proxy chain where the visitor is the first hop | xff-first |
first X-Forwarded-For entry |
| Platform that appends the last hop | xff-last |
last X-Forwarded-For entry |
| Cloudflare in front | cf-connecting-ip |
CF-Connecting-IP |
| Browsers connect to the proxy directly | socket |
none (socket address) |
A missing or malformed header falls back to the connecting socket’s address — the proxy never fails a request over it.
3. Add the install line
Add one script tag to your pages — anywhere in the document:
<script defer src="/px/t.js" data-site="pk_your_site_key"></script>
The data-site value must be your site key — the same value as
AURASCOPE_SITE. What matters is the meaning, not the exact bytes: a deferred
script sourcing /px/t.js carrying your data-site, anywhere in the document.
Templating frameworks, attribute reordering, and minifiers are all fine.
This line is frozen. It is stable for the life of your installation — every future upgrade ships behind it, and no version of this product will ever ask you to edit it. If you already have it installed, do not change it.
Verify your install
-
The snippet is served. From anywhere:
curl -sS -D - -o /dev/null https://www.your-site.example/px/t.jsExpect
200,content-type: text/javascript; charset=utf-8, andcache-control: public, max-age=300, stale-while-revalidate=86400. -
A real pageview round-trips. Open your site in a browser with DevTools → Network. You should see
/px/t.jsreturn200and, on each page load and each in-app navigation that changes the pathname, a/px/erequest return204. Navigations that change only the query string or hash intentionally send nothing — a pageview is a pathname change. -
The relay is healthy. A healthy proxy log is quiet: the single
listeningline and norelaying unsignedwarning. The204you see in the browser plus a silent log is the healthy shape. -
End-to-end confirmation. There is no self-serve install checker yet, so the final confirmation — your test pageviews visible as durably stored rows — comes from our team during onboarding. Ask us to confirm once steps 1–3 pass. This step is the only one that catches a wrong
AURASCOPE_INGEST_URL: steps 1–3 still look healthy with a mistyped endpoint, because fail-open means the browser-facing side never depends on the relay succeeding.
If you probe POST /px/e by hand with curl, you will get 204 whatever body
you send — that is the fail-open contract, not proof of a stored pageview. A
junk or malformed body is recorded on our side as a payload fault rather than a
pageview, and even for a well-formed body the 204 only proves the proxy
answered, never that anything was stored; use the browser check in step 2 for
the real thing.
Failure modes — and what they do to your site
The answer to every row in this table is nothing happens to your site. This is structural, not aspirational — each mechanism below exists precisely so that a broken analytics backend cannot become your outage. The worst case — a backend that accepted connections and then never answered (a hang, not a clean refusal) — was exercised in the end-to-end run with a real browser; the body-cap, body-deadline, and snippet-cache rows are held green by the proxy’s automated fail-open checks on every change, and the remaining rows follow from the construction each row’s “why” column shows.
| Failure | What the visitor experiences | Why |
|---|---|---|
| Ingest slow, paused, or unreachable | Nothing. The beacon still gets its 204 within milliseconds (the run measured 0.8–20 ms against a fully hung backend, versus 0.8–2.8 ms healthy), and DOMContentLoaded was no slower than the healthy baseline. |
The 204 is sent before the relay starts. The relay runs in the background on a held promise and is aborted at the 2 s relay deadline; failures are counted, never retried into your request path. |
| Oversized beacon body (over 65,536 bytes) | Nothing — still 204. |
The body read stops at the cap; the partial body is dropped and counted, never relayed. Without this cap, a hostile visitor could couple your traffic volume to the proxy’s memory. |
| Slow-trickle or stalled beacon body | Nothing — still 204, within the 250 ms body deadline. |
The read has a hard deadline, so a withholding client can neither delay the response nor pin connections open. A real browser beacon arrives fully flushed and reads in microseconds. |
| Snippet host unreachable, cache warm | Nothing — the cached snippet keeps being served, fresh for 5 minutes and then stale-while-revalidate for up to 24 hours, while one deduplicated background refresh retries. A failed refresh keeps serving the last good copy. | Serving stale never waits on the refresh — the cached bytes are returned immediately. |
| Snippet host unreachable, cache cold (first request ever) | The page renders normally without measurement; the script request fails cleanly (503, empty body, not cached). |
The install line is a deferred script — a script that fails to load is simply skipped by the browser; it never blocks rendering. |
| Signing misconfigured or secret missing | Nothing — one log warning, and relays continue unsigned. | Measurement degrades in confidence (pageviews are kept without the verified-origin mark), never in availability. |
| Proxy process down | The page renders normally; /px/* requests fail at your router until it restarts. |
The snippet is deferred and the beacon is fire-and-forget (navigator.sendBeacon) — neither is ever on your page’s critical path. |
Two things the proxy deliberately does not do: it never answers a beacon
with an error status (fail-open means 204 even for a body it must drop — a
dropped beacon is counted on our side, never silently pretended into storage),
and it never retries beacons from your infrastructure (it is not a durability
layer; durability is handled behind the ingest endpoint).
Troubleshooting
The start command fails with a bad-option error
Your Node is older than 22.6, which is where the type-stripping flag first shipped. Upgrade Node, or run the same entrypoint under bun.
/px/t.js returns 404 from your site
Your router is not sending /px/ to the proxy. Check the route added in step 2 —
the proxy itself binds loopback only and is never reachable directly.
/px/t.js returns 503
The proxy could not reach the snippet origin and had nothing cached. Check
AURASCOPE_SNIPPET_ORIGIN. The response is deliberately uncacheable, so a fixed
origin recovers on the next request — and because the install line is a deferred
script, your pages rendered normally throughout.
The log says provenance signing is not configured; relaying unsigned
One of the site key, key ID, or secret is missing from the environment. Pageviews are still relayed and kept; they simply carry no verified-origin mark until all three are set.
/px/e returns 204 but no pageviews arrive
204 is the fail-open contract, not proof of storage. The usual cause is a wrong
AURASCOPE_INGEST_URL — the one misconfiguration every browser-side check still looks
healthy for. Confirm with us (verify step 4).
Pageviews are counted as claimed rather than verified
The visitor IP is not being read where your topology actually carries it. Re-check
AURASCOPE_IP_STRATEGY against the table in step 2 — a missing or malformed header
falls back to the socket address rather than failing.
A filter or sort click sends nothing
Correct behaviour, not a fault: a pageview is a pathname change, so query-string and hash changes deliberately send nothing.
Uninstall
- Remove the install line from your pages.
- Remove the
/px/route from your router or CDN config. - Stop the proxy process and delete its environment configuration, including
the provenance secret from your secret store, then delete the
aurascope-proxy/directory.
That is everything. The proxy keeps no state on your infrastructure beyond its in-memory snippet cache, and the snippet set no cookies and stored nothing in your visitors’ browsers — there is nothing to clean up client-side.