I added PostHog to Tabeer.ai, a Nuxt 3 / Django platform I work on, to get real product analytics — which pages students actually use, where they drop off, that kind of thing — on top of the crash reporting (Sentry) and session recordings (Smartlook) already running there. The integration itself is simple. Getting to the point where it actually worked took a few wrong turns worth writing up, because the failure modes aren't well documented anywhere.
The Key That Didn't Work
I was handed a string starting with phs_ and told it was the "project secret id." PostHog project API keys — the ones you put in client-side tracking code — start with phc_. Personal API keys, used for account-level API access, start with phx_. phs_ isn't a format PostHog issues at all, at least not one I could find documented.
Rather than wire up a key that might silently fail in production, I checked it first:
curl -s -X POST "https://us.i.posthog.com/decide/?v=3" \
-H "Content-Type: application/json" \
-d '{"api_key":"phs_..."}'
Both PostHog Cloud regions (us.i.posthog.com and eu.i.posthog.com) came back with authentication_failed. That one curl call saved a deploy cycle of "why isn't anything showing up in the dashboard."
Trying the Official Wizard
PostHog ships @posthog/wizard, an interactive CLI that auto-detects your framework and wires everything up, including the client key, for you:
npx -y @posthog/wizard@latest
Two problems, both worth knowing about if you're running this from an automated environment (a CI job, an agent, a remote shell) rather than a local interactive terminal:
- It requires a TTY. In a non-interactive shell it exits immediately with
PHW_CLI_INTERACTIVE_REQUIREDand points you at--cimode instead. --cimode isn't available in the published build. Even with a validphx_...personal API key and--region us, the CLI refuses withPHW_CLI_FLAG_UNAVAILABLE: CI mode is not supported in published builds.
So in practice, right now, the wizard is a local-machine, human-in-the-loop tool. If you need to script the setup, you're doing it manually.
The Manual Fix
Once I had an actual personal API key (phx_..., generated from PostHog → your avatar → Personal API keys, scoped to project:read/project:write), getting the real project token was one API call:
curl -s "https://us.posthog.com/api/organizations/" \
-H "Authorization: Bearer phx_..."
The response includes each team's api_token — the actual phc_... client key — along with the project's region. That's the value that goes into the front-end snippet; the personal key never should.
Why I Didn't Add the npm Package
Tabeer.ai's frontend deliberately avoids npm packages for third-party analytics — Sentry, Smartlook and GA are all loaded via a plain <script> snippet, gated behind cookie consent, rather than through SDK packages that add SSR complexity and bundle weight for something that only ever runs client-side anyway. PostHog follows the same pattern: the official JS snippet, loaded dynamically, only after a visitor accepts the cookie banner.
Two config values went into nuxt.config.ts's public runtime config:
posthogKey: process.env.NUXT_PUBLIC_POSTHOG_KEY || '',
posthogHost: process.env.NUXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com',
And the load call, alongside the existing Smartlook loader in the cookie-consent component:
window.posthog!.init(config.public.posthogKey, {
api_host: config.public.posthogHost,
person_profiles: 'identified_only',
})
person_profiles: 'identified_only' is worth calling out specifically — without it, PostHog creates a full person profile (and counts a billable "monthly identified user") for every anonymous visitor, not just the ones who actually log in. On a free or early-stage paid plan, that difference matters.
The Actual Checklist
If you're doing this yourself, skip the trial and error:
- Get the project API key (
phc_...) from Project Settings, not a key from anywhere else. - If a key doesn't match
phc_(client) orphx_(personal), don't wire it up — verify it against/decide/first. - Know your region before you start —
us.i.posthog.comvseu.i.posthog.comisn't interchangeable, and a project on one region will silently reject calls aimed at the other. - If you want the wizard, run it locally and interactively — it's not (yet) a CI-friendly tool.
- Gate analytics loading behind actual cookie consent, not just because it's good practice — in the EU/UK it's a legal requirement, and it's a five-line
ifstatement to do properly.
If you're setting up analytics on a Nuxt or Django project and want a second pair of eyes on the architecture, get in touch.
By Shahid Malik