All Articles
Analytics & Tracking

Integrating PostHog Analytics Into a Nuxt + Django SaaS: A Real Walkthrough

Shahid MalikBy Shahid MalikSeptember 4, 20267 min read

How I wired up PostHog product analytics for Tabeer.ai, a Nuxt 3 / Django exam-prep platform — including the wrong API key format, why the official setup wizard wouldn't run, and the manual fix.

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:

  1. It requires a TTY. In a non-interactive shell it exits immediately with PHW_CLI_INTERACTIVE_REQUIRED and points you at --ci mode instead.
  2. --ci mode isn't available in the published build. Even with a valid phx_... personal API key and --region us, the CLI refuses with PHW_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:

  1. Get the project API key (phc_...) from Project Settings, not a key from anywhere else.
  2. If a key doesn't match phc_ (client) or phx_ (personal), don't wire it up — verify it against /decide/ first.
  3. Know your region before you start — us.i.posthog.com vs eu.i.posthog.com isn't interchangeable, and a project on one region will silently reject calls aimed at the other.
  4. If you want the wizard, run it locally and interactively — it's not (yet) a CI-friendly tool.
  5. 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 if statement 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.

Related Articles

Security

Rate Limiting, Token Blacklisting, and Admin MFA — Auth Hardening for Tabeer.ai

Part two of the production-readiness checklist: authentication and account security. Five of fifteen items were genuinely missing — rate limiting, real logout, admin MFA, consent records, and self-service data export/deletion — and here's exactly how each got fixed without a new dependency for most of them.

10 min read
Shahid Malik - AI-First Odoo Consultant

Shahid Malik

AI-First Odoo ERP Specialist

Shahid Malik is an AI-first Odoo consultant helping businesses solve complex ERP and business process challenges. His work combines Odoo consulting, process optimization, automation, integrations, migrations, and practical AI solutions to build scalable and reliable business systems.

Book a consultation for your Odoo project
Discuss Your Odoo Project