How to Track SaaS Conversions on Vercel Using PostHog and Google Search Console
You built the app in a weekend and shipped it to Vercel. You added a signup link, pasted in a couple of analytics snippets, and called it a day. Now you have three sources of numbers. Vercel Analytics shows visits. One analytics tool shows events. Google Search Console shows queries. None of them agree, and none of them talk to the other two.
The problem is not that you lack data. You have plenty. The problem is that the data sits in separate silos, so you cannot answer the one question that matters: which search query became a paid customer.
The fix is not another dashboard. It is making the sources talk. Once search queries, page views, and billing events share one system, you can answer questions like: is my revenue coming from a handful of keywords, and if so which ones? Which landing page converts organic traffic ten times better than the page next to it? That level of sight is what moves an app from traffic pump to actual business.
This post walks you through wiring PostHog into a Vercel app, linking Google Search Console as a data source, and building one dashboard that connects search impressions to signups to paid customers. No data warehouse required.
Why PostHog instead of GA4 or Vercel Analytics
Vercel Analytics is good at exactly one thing, and it is worth keeping for that thing. It takes two minutes to add, costs nothing within your plan, and gives you a traffic baseline: page views, top pages, core web vitals, referrers. For a solo builder it is the easiest zero-config dashboard you can switch on.
But Vercel Analytics is traffic analytics, not product analytics. It cannot tell you that a visitor from a specific search query signed up, started a trial, and paid. It does not track business events at all.
That gap is where PostHog fits. PostHog bundles product analytics, session replay, and feature flags in one tool, so you learn what users do inside your app, not just where they came from. It offers a generous free tier, which matters when you are pre-revenue. Its autocapture records clicks and events automatically, so you have useful data from the moment the SDK loads, before you define a single custom event.
Session replay is the underrated part of that bundle. When a query converts worse than it ranks, the recording shows you why: the user scrolled, hit a confusing form, and left. Feature flags let you test a new CTA on one segment of organic traffic while you wait for the next Search Console sync.
GA4 is the alternative everyone suggests, and it works. But PostHog stores named, typed events you can query directly, and autocapture gives you more signal than GA4's default setup on a small app. When your goal is tying paid conversions back to search, PostHog is the less tedious road.
Install via the Vercel Marketplace
The cleanest path is the Vercel Marketplace integration. Open your project, go to Settings, then Integrations, search for PostHog, and install it. You get two choices.
Add creates a brand-new PostHog org and bills you through your Vercel account. Link Existing Account connects an org you already run, which is the right call if PostHog has been collecting events for a while. Linking requires admin access on the existing org, and in that case billing stays with PostHog. As of September 2026 those are still the two ways the integration connects.
Whichever you pick, the integration pushes two environment variables into your Vercel project: NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN, your project key, and NEXT_PUBLIC_POSTHOG_HOST, the host for your instance, either us.i.posthog.com for the US region or eu.i.posthog.com for the EU region.
One timing detail: the integration writes the variables, but they only reach your running app on the next build. After you add or link the integration, kick off a fresh deployment, or your app keeps running with no PostHog values attached.
Check the prefix. The defaults work for Next.js, which inlines NEXT_PUBLIC_ values at build time. Vite and SvelteKit read VITE_ instead, so rename them to VITE_POSTHOG_PROJECT_TOKEN and VITE_POSTHOG_HOST. Nuxt wants NUXT_PUBLIC_. Get this wrong and the browser never sees the values, the page loads fine, and PostHog quietly records nothing.
Init the SDK in your app
With the environment variables in place, initialize the SDK once in your app entry point.
posthog.init(
process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN,
{
api_host:
process.env.NEXT_PUBLIC_POSTHOG_HOST ||
"https://us.i.posthog.com",
},
);
Where it lives depends on your framework. In a Next.js App Router app, put it in the root layout or a provider component that wraps the whole tree. In a Vite SPA, put it at the top of main.ts. The rule is one init per page load, before any component fires a capture call.
Autocapture and session replay come on by default. Keep autocapture; it is the cheapest way to see what users click. Turn off session replay unless you actually review recordings, because replay inflates your data volume and can push you toward a paid plan sooner than you expect.
One more thing: ad blockers eat PostHog requests by default. The standard fix is a reverse proxy so the request looks first-party. As of September 2026 PostHog Cloud offers a managed proxy at no extra cost. On Vercel you can do the same with a rewrite in next.config or vercel.json that forwards /ingest to your api_host. Whatever you do, avoid an obvious path like /analytics.
Define the conversion events that matter
Autocapture alone will not answer the money question. Define the events that mean revenue:
- signup: an account was created
- trial_start: a free trial began
- checkout_initiated: a user clicked pay
- paid_subscription_created: money landed
Each is a capture call at the moment it happens. A checkout confirmation is the classic spot:
posthog.capture("checkout_completed", {
plan: "pro",
});
A Stripe webhook is the cleanest feed for the paid event. Map checkout.session.completed to paid_subscription_created, read the plan from the subscription metadata, and pass the user id from a custom field on your checkout session. That way the event carries everything a dashboard needs: who, what plan, and when.
Fire paid_subscription_created from a server you trust, not from the browser. Anyone can forge a front-end event, and ghost subscribers corrupt every dashboard built on them. Your subscription webhook is the right source.
If you track from a Vercel serverless function, use the posthog-node SDK with captureImmediate. Serverless functions freeze and shut down after the response returns, and captureImmediate flushes the event before the function disappears, so the webhook that made you money is never lost to a cold shutdown.
Link Google Search Console
Now for the joining trick. PostHog can ingest Google Search Console as a data source, which lets you combine the queries people typed into Google with the events they fired once they clicked.
In PostHog go to Data pipeline, then Sources, then Google Search Console. Sign in with the Google account that has read access to the property, then enter the property. Both forms work: the URL-prefix form for a single exact site, and the sc-domain form for a whole domain, subdomains included.
Pick the property form deliberately. Choose sc-domain when an app and a marketing site share a domain across separate Vercel projects, because it rolls impressions from every subpage and subdomain into one view. Choose URL-prefix when you only care about a single exact path.
It syncs daily and gives you these tables: search_analytics_by_date, search_analytics_by_query, search_analytics_by_page, search_analytics_by_country, search_analytics_by_device, search_analytics_by_query_page, and search_analytics_by_search_appearance.
search_analytics_by_query_page is where you will live. It pairs each query with the exact page it landed on, which is the foundation for the dashboard below.
One limit worth knowing: the Google API caps the sync at roughly 50,000 rows per day. Very high-traffic properties drop the long tail. For most indie apps, a non-issue.
Build the query to conversion dashboard
The point of all this wiring is correlation. You join GSC query and page data with PostHog $pageview events, either in a PostHog SQL insight or the query builder, then join that to signup and paid events. You end with answers Google will not give you.
A practical dashboard has four panels:
| Panel | What it shows | Why it matters |
|---|---|---|
| Top queries that convert | Queries joined to signup and paid events, ranked by conversion rate | Your list of keywords that actually make money |
| Pages with impressions but low signups | Pages ranking well whose visitors never sign up | Content mismatched to the query, your fix list |
| Funnel: landing page to paid | Pageview to signup to paid per landing page | Shows exactly where organic traffic drops off |
| Queries with clicks, no conversions | Queries earning clicks that produce nothing | The quickest SEO wins hidden in your data |
In PostHog this is a SQL insight, and the query is short: select the query and page, count signup events, count paid events, group by query and page, sort by paid events descending. If SQL is not your language, the query builder does the same join with clicks. Save the result as a dashboard and add the weekly email subscription, so the report finds you instead of you hunting for it.
The workflow is then straightforward. Pick a query that ranks well but converts poorly. Open the page it lands on. Fix the mismatch: move the CTA above the fold, answer the question the query implies, cut friction in the form. Remeasure the same query next week. If conversion does not move, the problem is the promise or the offer, not the page. Iterate.
If you run generated page sets, the same join tells you which URLs earn their place. That is the logic behind programmatic SEO, measured against revenue instead of rank.
Verify the whole chain
How do you know it works? In order:
- Load the app, open DevTools, and filter the Network tab for your PostHog host. You should see requests on page load. None at all means the browser lacks the env vars, the proxy path is wrong, or an extension is blocking.
- Open PostHog Live events, reload the page, and watch the events appear within seconds.
- After the first daily sync, confirm rows exist in the GSC source tables inside PostHog. Give it a full day before you investigate further.
Gotchas I hit so you skip them:
- Missing public prefix. Without NEXT_PUBLIC_, VITE_, or NUXT_PUBLIC_, the SDK initializes with an undefined token and fails silently.
- Dev traffic pollution. Skip the init in development builds, for example by guarding it on your production environment, or local sessions drown the real signal.
- Region mismatch. If the host is eu.i.posthog.com but the env var or proxy says us, events never arrive.
- Consent. EU traffic needs consent before tracking under GDPR. GA4 has its own consent mode, and PostHog autocapture has consent-tool considerations too. Pick one banner and wire it to both, or capture EU traffic only after opt-in.
- Anonymous users. Call posthog.identify with the user id after login, so events before and after signup attach to the same person. Skip it and every session looks like a brand new stranger, which wrecks funnel math.
The loop
The loop is not faster SEO. It is better SEO. Rank, measure, fix, repeat. You stop guessing which blog posts matter and start knowing, because the revenue column tells you.
When a query, the page it hit, the trial it started, and the subscription it produced sit in one view, search becomes a sales channel instead of a traffic pump. That is the whole upgrade.
Pair this setup with the wider picture: how solo builders generate high-intent organic traffic with programmatic SEO, and how to rank an AI app on Google with zero ad spend. Then come back and let your own numbers decide what to build next.
The dashboard does the arguing for you. Set it up once, and every page you ship has a known job.
Want this done for your product?
Crawled SEO helps founders and small teams get found, cited, and recommended in Google and AI search. If you have an app with no traffic and want it fixed properly, that is exactly what we do.
Request your free audit