Skip to content
Zayne Lovecraft
All posts

Architecture · · 4 min read · Updated

Architecting Zivvi: Hybrid Search & Traffic Rankings in Next.js 16

How I built zivvi.app to index 2,400+ web apps using daily Tranco 1M traffic deltas, Upstash Vector + Postgres hybrid search, and Next.js 16 Partial Prerendering.

Zayne Lovecraft

Software Engineer III

Geometric 3D render of a blue cube with glass panels on top, against a blue background.
Photo by Yusuf P on Pexels

Launch directories have a structural flaw: rankings spike for 24 hours when a founder rallies upvotes, then freeze into a static graveyard. Meanwhile, traditional B2B review portals rank software by whoever pays for the top sponsored slot.

I built Zivvi (case study) to solve both problems at the data layer. Zivvi indexes over 2,400 web applications across 28 categories, separating objective global internet traffic from first-party community engagement and serving the entire catalog through Next.js 16 Partial Prerendering (PPR).

Here is how the ranking pipeline, hybrid search engine, and caching architecture work under the hood.

1. Separating global traffic from launch-day hype

If you put a brand-new indie SaaS tool and a global platform like Notion or Linear into a single leaderboard, either the indie app is invisible or the global rankings look fake. Zivvi splits discovery into distinct, verifiable signals:

  1. Global Web Rank (`/popular`): Every night, a scheduled cron job ingests the full Tranco top-1M domain list (which aggregates 30-day traffic data from Chrome UX Report, Cloudflare Radar, Cisco Umbrella, and Majestic). Each published app's canonical hostname is matched against the daily snapshot to record web_rank and a rolling 28-day baseline (web_rank_28d).
  2. 7-Day Momentum (`/trending`): Instead of raw traffic volume, trending scores combine 7-day first-party velocity—upvotes ($\times 3$), verified reviews ($\times 5$), and unique outbound clicks through /go/[slug]—with 28-day domain rank climbs ($\Delta = \text{web_rank_28d} - \text{web_rank}$).
  3. Cooling Off & Rising Leaders: Because we store daily rank time-series in Postgres, Zivvi also surfaces which established products are gaining or losing global web share over a 28-day window.

One non-obvious lesson from ingesting raw Tranco data: machine traffic looks like popularity if you don't filter it. Ad-tech SSPs, telemetry endpoints, NTP pools, and CDN domains rank in the global top 5,000 purely from background SDK pings. We maintain an explicit domain skip-list and hostname normalizer so only real, user-facing web applications enter the catalog.

2. Hybrid search: fusing Upstash Vector with Postgres FTS and pg_trgm

Users search an app store in two completely different ways:

  • Lexical lookup: "supabase", "linear", or a typo like "posthogg".
  • Intent lookup: "offline AI assistant that runs locally on iPhone" or "academic research workspace with LaTeX citations".

Pure vector search is mediocre at exact prefix and typo matching, while Postgres full-text search (tsvector) misses semantic paraphrases. Zivvi runs both in parallel and fuses the results:

// Parallel lexical (Postgres RPC) + semantic (Upstash Vector) retrieval
const [lexicalResult, vectorHits] = await Promise.all([
  supabase.schema("zivvi").rpc("search_products", {
    p_query: query,
    p_category: categorySlug ?? null,
    p_pricing: pricing ?? null,
    p_limit: 40,
  }),
  queryVectorIndex(query, { topK: 30 }),
]);

On the Postgres side, zivvi.search_products combines:

  • Weighted tsvector ranking across name (A), tagline (B), tags (B), and description (C).
  • Trigram similarity (pg_trgm) on product names and slugs so typos still resolve in a single round-trip.
  • Category authority boosting so canonical leaders in a category outrank tangential keyword matches.

When an admin approves a new submission or edits a listing in /admin, a Next.js after() callback re-embeds the product (text-embedding-3-small) and upserts its vector into Upstash without blocking the moderation response.

3. Next.js 16.4 Cache Components and Partial Prerendering (PPR)

With 2,400+ product pages (/apps/[slug]), alternatives hubs (/alternatives/[slug]), stack pages (/built-with/[tool]), and category leaderboards (/categories/[slug]), building every permutation statically at deploy time doesn't scale—and rendering every request dynamically hurts Time to First Byte (TTFB).

Zivvi runs on Next.js 16.4 with cacheComponents: true and partialPrefetching: true. As I covered in Understanding Client Components and Client Boundaries in Next.js, keeping the route shell deterministic lets Next.js stream an instant static shell (ShellStatic) on link hover while resolving dynamic data inside <Suspense> boundaries:

  • 5-minute minimum shell stale time (MIN_SHELL_STALE = 300): In Next.js 16.4, any "use cache" function with a stale window under 300 seconds is excluded from PPR app shells. All catalog read RPCs cache with cacheTag invalidation (product:{slug}, products, categories) so mutations in /admin or user upvotes call updateTag() for immediate consistency.
  • Zero-layout-shift streaming: Dynamic [slug] routes wrap await params in a <Suspense> boundary with a geometry-matched skeleton so forward and back navigation feel instantaneous.

4. Programmatic SEO & agent-readable endpoints

Every listing on Zivvi participates in a connected entity graph—similar to the patterns in Programmatic Technical SEO: Internal Links & Sitemaps at Scale and Generative Engine Optimization (GEO) & CWV Gates in Next.js:

  • Segmented sitemap index (/sitemap.xml): Splits URLs into child sitemaps (pages, apps, categories, blog, hubs) so Google Search Console reports indexation coverage per content type.
  • Semantic alternatives (/alternatives/[slug]): Combines category peers, shared stack tags, and vector similarity to generate clean, zero-fluff comparison pages.
  • Maker & AI interfaces: Verified makers can claim unclaimed community nominations, link public GitHub repositories for live star/fork telemetry, and embed dynamic SVG upvote badges (/badge/[slug]). For AI agents and answer engines, Zivvi exposes /llms.txt and a public JSON API at /api/v1/apps.

Takeaway

Building a modern directory that compounds organically requires treating rankings as a data engineering problem rather than a popularity contest. You can explore the live rankings at zivvi.app, browse the global traffic leaderboard, or read the full Zivvi architecture case study.

  • Next.js 16
  • Supabase
  • Upstash Vector
  • Technical SEO
  • Search Architecture

Keep reading