First-party proxy
@plaintrace/server runs next to your app. It serves the tracker from your own domain (past ad blockers),
records crawler reads that no browser script can see, counts human page loads so reports show measured
loss, and sends events from your backend. It uses web standards only (Request, Response, fetch,
crypto.subtle): Node 24+, Bun, Deno and Cloudflare Workers.
npm install @plaintrace/serverimport { createClient } from '@plaintrace/server'
const analytics = createClient({ key: process.env.ANALYTICS_KEY!, // an ingest key: never ship it to a browser endpoint: 'https://collect.plaintrace.dev', ipHeader: 'x-real-ip',})
// In your middleware, before the app:const res = await analytics.handle(request, { waitUntil: ctx.waitUntil })if (res) return resThen point the browser at your proxy path with data-endpoint (for example /_i).
The visitor’s address
Section titled “The visitor’s address”A visitor can send any header themselves, so by default only Cloudflare Workers’ CF-Connecting-IP is trusted.
Elsewhere set ipHeader to the one header your own edge overwrites:
| Platform | ipHeader |
|---|---|
| Vercel | x-vercel-forwarded-for |
| Fly.io | fly-client-ip |
| nginx | x-real-ip |
| Other proxies | x-forwarded-for (read from its right end) |
Or pass ip yourself.
What reaches the collector
Section titled “What reaches the collector”Only the user agent, language, origin, referer, Sec-Fetch-*, client hints and Sec-GPC. Cookies, your own
Authorization and Host never do. Every call is signed with HMAC over the method, path, body and time.