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.js returns 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/e on 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' and connect-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.comyour ingest endpoint from onboarding AURASCOPE_INGEST_URL
Snippet origin https://snippets.example-analytics.comyour 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-types flag 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.js and POST /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 proxy
  • POST /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

  1. The snippet is served. From anywhere:

    curl -sS -D - -o /dev/null https://www.your-site.example/px/t.js

    Expect 200, content-type: text/javascript; charset=utf-8, and cache-control: public, max-age=300, stale-while-revalidate=86400.

  2. A real pageview round-trips. Open your site in a browser with DevTools → Network. You should see /px/t.js return 200 and, on each page load and each in-app navigation that changes the pathname, a /px/e request return 204. Navigations that change only the query string or hash intentionally send nothing — a pageview is a pathname change.

  3. The relay is healthy. A healthy proxy log is quiet: the single listening line and no relaying unsigned warning. The 204 you see in the browser plus a silent log is the healthy shape.

  4. 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

  1. Remove the install line from your pages.
  2. Remove the /px/ route from your router or CDN config.
  3. 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.

nic@aurascope.co