Spoar

How it works

What happens to an event between the browser and the database, and where each privacy guarantee comes from.

  1. Browser SDKBatches events, sends them with fetch or sendBeacon
  2. /_ra proxySame-origin route in your app, forwards to the API
  3. API /v2/eventsChecks key and origin, scores bots
  4. EngineEnriches with geo, device and channel
  5. PostgresStores the row; reads, dashboard and SQL answer from it

The server SDK skips the proxy and posts to /v2/events with the secret key.

In the browser

  1. analytics.track("signup", { plan: "pro" }) builds an event with a UUIDv7 id, the name, the time, the visitor and session ids, the page (path, route template, title, and the referrer on the first pageview) and props.
  2. Props are kept inside the limits: at most 25, flat primitive values, keys and strings up to 255 characters. Anything outside is shortened or left out, with an RA_PROPS_LIMITED warning in the console.
  3. beforeSend and plugins may change or drop the event. Consent, opt-out, Do Not Track and Global Privacy Control are checked here; a dropped event never leaves the browser.
  4. Events wait in a queue and go out together: after 20 events, after 5 seconds, or when the page is hidden. A batch is cut short to keep its body under 60 KB, and a single event over 60 KB is dropped. They are sent with fetch and keepalive, or with sendBeacon when the page is being left.
  5. A failed send retries after 1, 4 and 16 seconds, or after the server's Retry-After up to 16 seconds. When the page is left, batches waiting for a retry are sent with the rest. Events that are still unsent are saved and sent on the next page load.

At the proxy

The browser posts to /_ra on your own domain, so ad blockers see a first-party request. The proxy:

  • accepts only POST, and only from the same site;
  • refuses bodies over 60 KB, the API's own limit;
  • adds the project's secret key, so the key never reaches the browser;
  • adds the visitor's IP and user agent as X-Visitor-IP and X-Visitor-UA, because the API only sees your server's address;
  • forwards the page's Origin, or the site's own origin when the browser sent none, so the API can flag localhost and preview hosts;
  • forwards the admin session cookie ra.session_token and no other cookie, so a signed-in admin's events are marked internal;
  • returns the API's answer unchanged.

Without a proxy the browser sends to the API directly with the public key, and the API only accepts it from the project's allowed origins.

In the API

The engine runs every batch through the same steps:

  1. Authorize. A public key must come from an allowed origin; a secret key is trusted.
  2. Rate limit. Callers with a public key are limited per IP hash, 100 requests a minute by default (INGEST_RATE_LIMIT).
  3. Parse. Each event is checked against the contract, prop limits included. An invalid event is rejected by its index; the rest of the batch still goes through.
  4. Enrich. Country, region, city and network (ASN) from local MMDB lookups of the IP; browser, OS and device from the user agent; UTM tags and the channel from the URL and referrer.
  5. Score bots. Known crawlers and automation user agents, datacenter networks, inconsistent headers and the client's botSignals add weight to a score from 0 to 100. Events are never dropped for their score; reads filter at 50 and above.
  6. Flag. Events from localhost, preview deployments and signed-in admins are marked, so your own traffic is left out of reports. The host comes from the request's Origin, and a signed-in admin is recognised by the session cookie, which only the proxy and the server client forward; a browser sending to the API directly carries no cookie.
  7. Store. Repeated event ids are counted as duplicates and skipped, the batch is written in one insert, and sessions and visitors are updated.

The answer lists how many events were accepted, how many were duplicates, and which were rejected and why.

What is stored, and what is not

StoredNot stored
A random visitor id from localStorage and a random session id from sessionStorageCookies of any kind for visitors
sha256(ip + sha256(secret + day)), for rate limits and countingThe IP address
Country, region, city and the city's approximate coordinates, from the IP lookupA street-level location
Path, route template, title and referrerQuery strings in error messages and breadcrumbs
The props you sendForm field values: the forms plugin reads only the form's id and action

The IP hash uses a salt that changes every UTC day, so a hash cannot link a visitor across days.

Reading the data

Every read goes through the API: headline numbers, timeseries, breakdowns, realtime, paths, retention, speed insights, issues and a read-only SQL console. A project is public by default, so its aggregates can be read without signing in; a private project answers only to its members and tokens. The Auth overview lists who can call what.

On this page