npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@posthog/convex

v1.0.10

Published

Capture analytics events, track exceptions, and evaluate feature flags with PostHog from your Convex backend.

Readme

🦔 What is this?

The official PostHog component for Convex. Capture events, identify users, manage groups, and evaluate feature flags — all from your queries, mutations, and actions.

Found a bug? Feature request? File it here.

🚀 Quick Start

Install the package:

pnpm add @posthog/convex

Register the component in your convex/convex.config.ts:

// convex/convex.config.ts
import { defineApp } from "convex/server";
import posthog from "@posthog/convex/convex.config.js";

const app = defineApp();
app.use(posthog);

export default app;

Set your PostHog credentials on your Convex deployment:

npx convex env set POSTHOG_API_KEY phc_your_project_api_key
npx convex env set POSTHOG_HOST https://us.i.posthog.com

To enable local feature flag evaluation, also set a feature flags secure API key (phs_…) with read access to feature flags:

npx convex env set POSTHOG_PERSONAL_API_KEY phs_your_feature_flags_secure_api_key

Personal API keys (phx_…) also still work for local evaluation, but PostHog recommends the project-scoped feature flags secure API key going forward.

Create a convex/posthog.ts file to initialize the client. Read the keys from process.env and pass them to the constructor — the client captures them and forwards them to component actions as needed:

// convex/posthog.ts
import { PostHog } from "@posthog/convex";
import { components } from "./_generated/api";

export const posthog = new PostHog(components.posthog, {
  apiKey: process.env.POSTHOG_API_KEY,
  personalApiKey: process.env.POSTHOG_PERSONAL_API_KEY,
  host: process.env.POSTHOG_HOST,
});

Schedule a cron in your own convex/crons.ts that refreshes the flag definitions on whatever interval suits you. The client class captures the keys you passed in posthog.ts and forwards them automatically:

// convex/crons.ts
import { cronJobs } from "convex/server";
import { internalAction } from "./_generated/server";
import { internal } from "./_generated/api";
import { posthog } from "./posthog";

export const refreshPosthogFlags = internalAction({
  args: {},
  handler: async (ctx) => {
    await posthog.refreshFlagDefinitions(ctx);
  },
});

const crons = cronJobs();
crons.interval(
  "refresh posthog feature flag definitions",
  { minutes: 1 },
  internal.crons.refreshPosthogFlags
);

export default crons;

That's the whole setup — feature flag methods will start returning live values on the next cron tick, or you can call posthog.refreshFlagDefinitions(ctx) from an action whenever you want an immediate refresh.

📊 Capturing Events

Import posthog from your setup file and call methods directly:

// convex/myFunctions.ts
import { posthog } from "./posthog";
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const createUser = mutation({
  args: { email: v.string() },
  handler: async (ctx, args) => {
    const userId = await ctx.db.insert("users", { email: args.email });

    await posthog.capture(ctx, {
      distinctId: userId,
      event: "user_created",
      properties: { email: args.email },
    });

    return userId;
  },
});

capture

Capture an event. Works in mutations and actions.

await posthog.capture(ctx, {
  distinctId: "user_123",
  event: "purchase_completed",
  properties: { amount: 99.99, currency: "USD" },
  groups: { company: "acme-corp" },
});

Options: distinctId, event, properties, groups, sendFeatureFlags, timestamp, uuid, disableGeoip.

identify

Set user properties.

await posthog.identify(ctx, {
  distinctId: "user_123",
  properties: { name: "Jane Doe", plan: "pro" },
});

groupIdentify

Set group properties.

await posthog.groupIdentify(ctx, {
  groupType: "company",
  groupKey: "acme-corp",
  properties: { industry: "Technology", employees: 500 },
});

alias

Link two distinct IDs.

await posthog.alias(ctx, {
  distinctId: "user_123",
  alias: "anonymous_456",
});

captureException

Send an exception to PostHog's error tracking pipeline. Accepts an Error, a string, or any object with a message field.

try {
  await chargeCard(...);
} catch (error) {
  await posthog.captureException(ctx, {
    error,
    distinctId: "user_123",
    additionalProperties: { plan: "pro" },
  });
  throw error;
}

If you'd rather have every uncaught error from your Convex deployment forwarded to PostHog automatically — including ones you didn't explicitly wrap — wire up Convex's first-party PostHog exception reporting integration from the Convex dashboard. Setup lives at docs.convex.dev/production/integrations/exception-reporting#configuring-posthog-error-tracking. Use captureException here for cases where you want explicit control (e.g. attaching custom additionalProperties); use the Convex-side integration for catch-all coverage.

All of the above methods schedule the PostHog API call asynchronously via ctx.scheduler.runAfter, so they return immediately without blocking your mutation or action.

🚩 Feature Flags

Two evaluation paths, pick the one that fits the flag:

  • Local (getFeatureFlag, isFeatureEnabled, …) — evaluates against definitions cached by the cron. Works in queries, mutations, and actions, no per-call network round-trip, reactive (a query reading a flag re-runs when definitions change). Requires POSTHOG_PERSONAL_API_KEY. Can't handle every flag — see the limitations below.
  • Remote (evaluateFlag, evaluateFlagPayload, evaluateAllFlags) — hits PostHog's /flags endpoint directly. Action-context only, no personalApiKey needed, handles every flag.

The local methods are documented first; remote is at the bottom of this section.

getFeatureFlag

Get a flag's value.

import { posthog } from "./posthog";
import { query } from "./_generated/server";
import { v } from "convex/values";

export const getDiscount = query({
  args: { userId: v.string() },
  handler: async (ctx, args) => {
    const flag = await posthog.getFeatureFlag(ctx, {
      key: "discount-campaign",
      distinctId: args.userId,
    });

    if (flag === "variant-a") {
      return { discount: 20 };
    }
    return { discount: 0 };
  },
});

isFeatureEnabled

Check if a flag is enabled.

const enabled = await posthog.isFeatureEnabled(ctx, {
  key: "new-onboarding",
  distinctId: "user_123",
});

getFeatureFlagPayload

Get a flag's JSON payload.

const payload = await posthog.getFeatureFlagPayload(ctx, {
  key: "pricing-config",
  distinctId: "user_123",
});

getFeatureFlagResult

Get a flag's value and payload in one call.

const result = await posthog.getFeatureFlagResult(ctx, {
  key: "experiment-flag",
  distinctId: "user_123",
});
if (result) {
  console.log(result.enabled, result.variant, result.payload);
}

getAllFlags

Get all flag values for a user.

const flags = await posthog.getAllFlags(ctx, {
  distinctId: "user_123",
});

getAllFlagsAndPayloads

Get all flags and their payloads.

const { featureFlags, featureFlagPayloads } =
  await posthog.getAllFlagsAndPayloads(ctx, {
    distinctId: "user_123",
  });

All feature flag methods accept optional groups, personProperties, groupProperties, and disableGeoip options. getAllFlags and getAllFlagsAndPayloads also accept flagKeys to filter which flags to evaluate.

Local evaluation — limitations

Local eval can't reach a verdict for every flag, and for those this component will return null. The cases:

  • Experience continuity flags. Flags with persist across authentication steps need server-side anon→identified tracking and aren't included in local eval.
  • Static cohorts. Cohort membership for static cohorts lives only on the server.
  • Properties not passed in. Local eval can only see what you give it. If a flag targets email or $browser_version and you don't pass those in personProperties, it can't resolve.
  • Cohorts that don't fit the local-eval shape. Cohorts with variant overrides, non-person properties, more than one cohort in the same flag definition, nested AND/OR filters, or grouped with other conditions can't be translated for local eval. See the PostHog docs for the full list.

Local eval doesn't fire $feature_flag_called events. PostHog Experiments counts exposures off these — posthog-node emits them automatically on every local eval, but this component can't do the same: Convex queries are pure functions, so they can't schedule a capture from inside the eval path without breaking Convex's contract. If you're running an experiment against a locally-evaluated flag, fire one manually from a mutation or action:

await posthog.capture(ctx, {
  event: "$feature_flag_called",
  distinctId: userId,
  properties: {
    $feature_flag: "flag-key",
    $feature_flag_response: value,
    locally_evaluated: true,
  },
});

There are also reasons you might not want local eval at all, even when it's possible:

  • Low-traffic projects. PostHog bills each /flags/definitions poll as 10 flag-request equivalents. For projects that evaluate fewer flags than that per polling interval, remote evaluation is cheaper.
  • Need-it-now changes. Local eval accepts up to one polling interval of staleness (default 1 minute with our cron). For flags that must flip in well under that, you want remote eval.
  • No personal API key. If you don't want to set POSTHOG_PERSONAL_API_KEY, the local methods aren't useful — there's nothing for them to read.

For any of those, use the remote-eval methods below instead.

Remote evaluation

Sibling methods that hit PostHog's /flags endpoint directly. They require an action context (each call is a network round trip) and don't need personalApiKey. They handle every case local eval can't.

import { posthog } from "./posthog";
import { action } from "./_generated/server";
import { v } from "convex/values";

export const getContinuityFlag = action({
  args: { userId: v.string() },
  handler: async (ctx, args) => {
    const value = await posthog.evaluateFlag(ctx, {
      key: "my-experience-continuity-flag",
      distinctId: args.userId,
      personProperties: { plan: "pro" },
    });
    return value;
  },
});

Three methods:

| Method | Returns | | --- | --- | | posthog.evaluateFlag(ctx, args) | FeatureFlagValue \| null | | posthog.evaluateFlagPayload(ctx, args) | JsonType \| null | | posthog.evaluateAllFlags(ctx, args) | { featureFlags, featureFlagPayloads } |

Same option shape as the local methods (groups, personProperties, groupProperties, disableGeoip, flagKeys on the all-flags variant). Pick local when the flag is suitable and the cost of /flags/definitions polling is justified; pick remote when it isn't.

📦 Example

See the example app for a working demo.

🤝 Contributing

See CONTRIBUTING.md for package-specific development instructions.

📄 License

MIT