# PostHog

> Track-only AnalyticsSink that forwards events to PostHog's /capture endpoint — compose it with a second sink that owns reads.



---

<!-- source: en/plugins/posthog.md -->
## PostHog

_Track-only AnalyticsSink that forwards events to PostHog's /capture endpoint — compose it with a second sink that owns reads._

`@voltro/plugin-posthog` is a **track-only** `AnalyticsSink` — it forwards every
`track()` to PostHog's `/capture` endpoint so your events show up in PostHog's
product-analytics UI (funnels, cohorts, sessions, feature flags). PostHog's
query model is too PostHog-specific to fit the cross-provider contract, so
`aggregate` / `timeseries` / `topN` return `AnalyticsCapabilityNotSupported`.
The intended wiring is therefore `composeAnalytics([...])`: a primary sink owns
the typed reads, PostHog additionally mirrors every event. For the shared
`AnalyticsSink` API and the `useAnalytics()` read methods, see
[Analytics & warehouse sinks](/docs/plugins/analytics).

## Install

```
pnpm add @voltro/plugin-posthog
```

## Wiring — compose with a read-capable sink

PostHog can't serve reads, so pair it with a primary sink via `composeAnalytics`
(`track()` fans out to both; reads route to the first sink that supports them):

```ts
// app.config.ts
import { composeAnalytics } from '@voltro/runtime'
import { postgresAnalytics } from '@voltro/plugin-analytics-postgres'
import { posthogAnalytics }  from '@voltro/plugin-posthog'

export default {
  type:      'api' as const,
  name:      'myApi',
  store:     'postgres' as const,
  analytics: composeAnalytics([
    postgresAnalytics(),                                     // primary — handles reads
    posthogAnalytics({ apiKey: process.env.POSTHOG_KEY! }), // also forwards every track()
  ]),
}
```

## Options

`posthogAnalytics(options)`:

| Option | Type | Default | Notes |
|---|---|---|---|
| `apiKey` | `string` | — (**required**) | PostHog project API key. |
| `host` | `string` | `'https://app.posthog.com'` | Use `'https://eu.posthog.com'` for EU cloud, or your self-hosted URL. |
| `batch` | `{ maxSize?, flushIntervalMs? }` | unset (immediate mode) | Opt into client-side batching to PostHog's `/batch/` endpoint. |
| `batch.maxSize` | `number` | `20` | Flush once the buffer reaches this many events. |
| `batch.flushIntervalMs` | `number` | `5000` | Flush a non-empty buffer this many ms after the first event lands. |

> **Connection details come from `options`, not env.** The plugin reads no
> `POSTHOG_*` variables itself — the example wires `process.env` into the
> options. Use `@voltro/env`'s `defineEnv` to declare the key as a secret.

## Delivery semantics

- **Immediate (default, `batch` unset):** every `track()` is one `/capture/`
  POST, so a successful `track()` means the event was delivered.
- **Batched (`batch: {...}`):** events buffer and flush by size, on a timer,
  and on graceful shutdown (the `dispose` hook). A successful `track()` then
  means "buffered" — far fewer round-trips under load, but a flush failure is
  logged + the batch dropped (best-effort; `/batch/` carries no idempotency
  key). A hard crash loses whatever is still buffered. Leave `batch` unset when
  you need per-event delivery confirmation.

## Capability limits

`aggregate`, `timeseries`, and `topN` always return
`AnalyticsCapabilityNotSupported` (provider `posthog`) — catch it with
`Effect.catchTag` for graceful fallback, or rely on `composeAnalytics` routing
reads to a read-capable sink. For PostHog-specific endpoints (capture batches,
decide, person properties) import the `PostHog` Tag and use the raw HTTP API.

## See also

- [Analytics & warehouse sinks](/docs/plugins/analytics) — the shared
  `AnalyticsSink` contract, the `useAnalytics()` read API, `composeAnalytics`,
  and capability-not-supported error handling.
