Pingpong is the first-party analytics and live-presence system for the sites I run. It is one Cloudflare Worker, one D1 database, two small Durable Object actors, and a Vite/React dashboard served from the same Worker.
The split
D1 owns every durable fact:
- idempotent event history and edge dimensions;
- the site registry derived from those events;
- page view counters and short-lived HMAC dedupe reservations;
- comments and their rate limits.
Durable Objects own only WebSocket coordination:
ActiveUsersis one actor per site. It hibernates idle sockets, deduplicates sibling tabs by a private visitor ID, and publishes redacted presence changes.DashboardHubis one global socket broker. It stores no analytics tables; new subscribers rebuild from D1 and the per-site presence actors.
The distinction is useful: history wants SQL, indexes, and cross-site queries. Presence wants a single actor that can say exactly which sockets are connected. A normal Worker handles validation, routing, privacy, cache policy, and edge enrichment without becoming another state owner.
One navigation, one fact
Sites install one hosted script:
<script defer src="https://pingpong.sdan.io/client.js"
data-site="sdan.io"
data-presence="true"></script>The client observes initial load and SPA navigation, assigns each event an idempotency key, and posts it to /collect. The Worker waits for D1 to accept the event. Only a new page-view event can increment the display counter or publish a dashboard frame, so network retries do not double count.
Presence uses one WebSocket owned by that same client. React components subscribe to its pingpong:active event instead of opening a second connection. The public feed receives a connection-scoped ID, city-level coordinates, and an allowlisted path; the stable browser ID never leaves the actor.
Reads and live updates
/stats, /total, /edge, /breakdown, view counts, comments, site lists, and dashboard summaries query D1. The expensive historical dashboard responses use a short POP-local cache with explicit fresh and stale windows. Request-specific edge data is appended after the cache lookup, and presence is never cached.
The dashboard WebSocket gets an initial snapshot from D1 plus ActiveUsers, then receives new event and presence frames from DashboardHub. If the hub is evicted, the facts remain intact and the next connection rebuilds the view.
Cost on sdan.io
The site loads the client after hydration, and its content renders when analytics is unavailable. The offline service worker never intercepts cross-origin Pingpong traffic. On weak connections, the last presence snapshot is visibly disconnected for a short reconnect grace period and then clears; cached visitors are never presented as live.