Building a Real-Time Trending Section With Supabase, RLS, and sendBeacon
Adding a live "trending tools" feature without adding a single millisecond to page load.
*Adding a live "trending tools" feature without adding a single millisecond to page load.*
A static "featured tools" section is fine, but a directory site benefits more from showing what's actually popular right now. The challenge was adding that feature — a real-time trending section — without undoing the performance work that had just brought PageSpeed from 69 to 92. The two goals seemed to pull in opposite directions: more data, more dynamism, but no added load time.
The Architecture
The solution split into three pieces that never block each other:
A tool_events table in Supabase records every meaningful interaction — a click, a view, a search result selection — as a row with a timestamp and a tool reference. This is the raw signal trending is calculated from.
An API endpoint at /api/track.js receives these events. Critically, it's called using navigator.sendBeacon rather than a normal fetch request. sendBeacon is designed exactly for this use case: firing off small amounts of analytics data without blocking the page, without waiting for a response, and without risking the request being cancelled if the user navigates away mid-request — a real concern on mobile connections where navigation can happen mid-click.
Server-side aggregation in lib/trending.js takes the raw event stream and turns it into a ranked list, computed on the server rather than the client. This keeps the client-side JavaScript bundle untouched by trending logic — the client just renders whatever list the server hands it.
Row Level Security Had to Be Right
Because tool_events is a public-facing table receiving writes from anonymous visitors, Row Level Security policies needed to allow inserts without allowing arbitrary reads or updates of other users' event data. Getting this wrong in either direction is a real problem — too permissive and the table becomes a spam vector or a privacy leak; too restrictive and legitimate tracking silently fails with no visible error on the frontend. A full Supabase security audit later confirmed the RLS policies and the SECURITY DEFINER functions handling aggregation were implemented correctly — but that verification happened after the initial build, as a separate, deliberate check.
What Happens If the Beacon Fails Anyway
Even sendBeacon isn't a perfect guarantee — it improves the odds of a tracking event surviving a page navigation dramatically compared to a standard fetch, but it doesn't make delivery certain under every possible failure mode, such as a genuine network drop mid-request rather than a page-unload race. The trending feature was designed with that imperfection in mind: it treats the event stream as a statistical signal rather than an exact count, meaning occasional dropped events shift the trending calculation negligibly rather than being treated as a correctness bug requiring guaranteed-delivery infrastructure that would be significant overkill for a "what's popular right now" feature.
Why sendBeacon Specifically
Using a standard fetch call for event tracking is the more obvious choice, and it would have worked most of the time. But sendBeacon exists precisely because standard fetch requests can be cancelled by the browser when a page unloads — which happens constantly on a directory site where users click through to a tool and immediately navigate away. Losing tracking events silently would have made the trending data unreliable in exactly the moments that mattered most: the click that led somewhere.
Deciding What Counts as a Trending Signal
Not every event should carry equal weight in a trending calculation, and getting this wrong produces a list that's technically "real-time" but not actually useful — dominated by whatever got the most raw clicks in the last hour regardless of whether those clicks reflected genuine interest or just placement on the page. The aggregation logic in lib/trending.js weights different event types differently: a full page view of a tool's detail page counts for more than a search-result click that never led anywhere, and recency is factored in so a tool that was popular three days ago doesn't outrank one trending in the last few hours. None of this is complicated math, but it required treating "trending" as a design decision with real trade-offs, not just a raw count sorted descending.
Handling the Edge Case of Bot Traffic
A publicly writable events table, reachable by any anonymous visitor, is also reachable by anything that isn't a real visitor — scrapers, bots, and automated crawlers that hit pages without any genuine user behind them. Left unaddressed, this would let bot traffic distort the trending list, potentially in an easily gameable way if someone specifically wanted to inflate a tool's ranking. The mitigation combined basic rate-limiting at the tracking endpoint with sanity-checking event patterns during aggregation — a burst of hundreds of events from what looks like a single session in a few seconds gets discounted rather than counted at full weight. This isn't a bulletproof anti-bot system, but it's a deliberate first layer rather than an assumption that raw event counts could be trusted as-is.
The Result
A trending section that updates based on real usage, calculated server-side, tracked through a non-blocking beacon API, and secured by RLS policies verified in a dedicated audit — all added without measurably affecting the page's Core Web Vitals. The trending feature became additive rather than a trade-off against the performance work that came before it.
📬 Get the latest AI tool reviews
Expert picks and comparisons, weekly. No spam.