Spoar

Send events through your own domain

Serve /_ra from your site with createProxy so events are first-party requests that ad blockers let through.

This guide moves browser events from the API's host to a path on your own site, /_ra. Ad blockers drop requests to third-party analytics hosts; a same-origin path is a first-party request. It needs a server or function on the same origin as the site. A fully static site without one sends to the API directly, with the public key.

Public key or secret key

Direct to the APIThrough /_ra
Browser endpointhttps://api.example.com/v2/events/_ra (the default)
Credential on the request to the APIPublic key pk_... from the browserSecret key sk_..., added by your server
Origin checkThe page's origin must be in the project's allowedOriginsThe proxy refuses cross-site requests itself
Visitor IP and user agentRead by the API from the browser's requestForwarded by the proxy as X-Visitor-IP and X-Visitor-UA
Host, localhost and preview flagsFrom the browser's OriginFrom the Origin the proxy forwards, or the site's own origin when the browser sent none
Signed-in admin marked internalNo: a cross-origin request carries no cookieYes: the proxy forwards the ra.session_token cookie and no other
Ad blockersOften blockedA request to your own domain

The secret key sends events for the project from anywhere, so it stays on the server. Never put it in a NEXT_PUBLIC_, PUBLIC_ or VITE_ variable.

1. Store the secret key

The secret key is shown once, in the response that creates the project. If you no longer have it, rotate it with an admin token for the project; the answer holds the new key in data.key, and the old key stops working:

curl -X POST https://api.example.com/v2/projects/example.com/keys \
  -H "authorization: Bearer $RA_TOKEN" \
  -H "content-type: application/json" \
  -d '{"kind":"secret"}'

Put it and the API's base URL in the server's environment:

RA_SECRET=sk_...
RA_ENDPOINT=https://api.example.com

These two names are only a convention of these docs. createProxy reads the JSON in RA_CONFIG for options you leave out, so RA_CONFIG='{"secret":"sk_...","endpoint":"https://api.example.com"}' works too.

2. Add the route

In the Next.js App Router, a folder starting with _ is private, so the underscore is URL-encoded.

app/%5Fra/route.ts:

import { createProxy } from "@spoar/sdk/proxy";

export const POST = createProxy({
  secret: process.env.RA_SECRET,
  endpoint: process.env.RA_ENDPOINT,
});

createProxy returns a standard (request: Request) => Promise<Response> handler, so any runtime with fetch, Request and Response can serve it. In a hand-written fetch handler, route /_ra to it:

import { createProxy } from "@spoar/sdk/proxy";

const proxy = createProxy({ secret: process.env.RA_SECRET, endpoint: process.env.RA_ENDPOINT });

export async function handle(request: Request): Promise<Response> {
  if (new URL(request.url).pathname === "/_ra") return proxy(request);
  return new Response("Not found", { status: 404 });
}

Server runtimes has complete examples for Bun, Deno, Vercel Functions and Cloudflare Workers, and Astro and Svelte show their own route files. On Vercel Functions, api/_ra.ts is not routed because of the underscore; serve the proxy from another path and set endpoint to it.

3. Point the client at it

/_ra is the default endpoint, so a client without one already uses it. With the config variable:

NEXT_PUBLIC_RA_CONFIG='{"project":"example.com","key":"pk_...","endpoint":"/_ra"}'

Keep key set. The proxy does not need it, but the client warns with RA_NO_KEY when it is empty. An empty key leaves the key query parameter out of the request. If you serve the proxy on another path, set endpoint to that path.

4. Check it

  1. Open the site with the browser's network panel. Events go to POST /_ra?key=pk_... and answer 202 with { "accepted": 1, "duplicates": 0, "rejected": [] }.

  2. Check that the route refuses what it should:

    curl -i https://example.com/_ra
    curl -i -X POST https://example.com/_ra -H "sec-fetch-site: cross-site" -d '{}'

    The first answers 405 and the second 403 with FORBIDDEN_ORIGIN.

Answer from /_raCause
500 INTERNAL, "needs a secret and an endpoint"secret or endpoint is empty on the server
401 UNAUTHORIZEDThe API rejected the secret key; check for a rotated key
413 PAYLOAD_TOO_LARGEThe body is over 60 KB
502 UNAVAILABLEThe proxy could not reach endpoint

5. Optionally count blocked pageviews

With the proxy in place, createPageCounter from the same entry counts HTML page loads in middleware as page_request events. Comparing them with pageviews estimates how many visitors block the client. See Proxy.

On this page