Introduction
The lifecycle automation layer PostHog doesn't have yet. Scaffold an app, define journeys in TypeScript, and ship your first lifecycle emails in minutes.
What is Hogsend?
Hogsend is one loop: an inbound event comes in, your code reacts, engagement flows back out. A PostHog webhook, a call from your own app, a Stripe event — they all normalize to a single thing: an event for a user. Your TypeScript reacts (journeys send/wait/branch, buckets segment in real time), and everything that happens — opens, clicks, sends, journey completions — fans back out to your analytics, your CRM, your warehouse. Not drag-and-drop canvases. Code.
If you're a PostHog team that's been putting off lifecycle emails because Customer.io is overkill and building it yourself is a rabbit hole — this is the thing you'd build anyway. We just built it first.
Install the engine, own your content
Hogsend ships as an npm package — @hogsend/engine — that your app installs like any other framework. You scaffold a fresh app that pins the engine and owns only your content: journeys, email templates, webhook sources, custom routes, config, and your own database migrations.
- The engine (
@hogsend/engineplus the other@hogsend/*packages) is the framework. Upgrade it withpnpm up "@hogsend/*"; when you need to go further than config, you Extend, Patch, or Eject. - Your content lives in your repo and is injected into the engine's factories (
createHogsendClient,createApp,createWorker). The engine never imports your content.
Why Hogsend exists
PostHog is incredible at product analytics. But when you want to act on what you see — send a nudge when someone drops off onboarding, recover a failed payment, re-engage a dormant user — you're on your own. Hogsend bridges that gap.
Think of it as a gateway drug into lifecycle automation. Start here, get scrappy, ship emails that move the needle. When you outgrow it — or when PostHog ships their own marketing automation — you'll have a clean event model and proven journeys to migrate.
PostHog-first, not PostHog-only
Hogsend is built for PostHog, but it's not locked to it. Stripe webhooks, custom API calls, any system that can send an HTTP request can feed events into Hogsend. All examples in these docs lead with PostHog because that's the primary use case, with Stripe as the common secondary source.
The email provider is swappable — Resend is the default (fast, developer-friendly, great deliverability). Postmark ships as an opt-in provider, and you can bring any other behind the EmailProvider contract — render, preferences, and first-party tracking come along no matter which wire you choose. The same lifecycle event stream fans out to Slack, PostHog, Segment, a CRM, or a warehouse through outbound destinations. Push notifications are on the roadmap.
How it works
- Scaffold your app —
pnpm dlx create-hogsend@latest my-appemits a thin app that pins@hogsend/engineand holds your content. - Events flow in — Point a PostHog webhook at your app, call
POST /v1/eventsfrom your own code, or wire anydefineWebhookSource()(Stripe, Clerk, your CRM). They all normalize to one event for a user throughingestEvent(). - Your code reacts — TypeScript functions that trigger on events, send emails, wait, branch, and adapt based on what the user does.
- Email sends through a swappable provider — Resend by default, Postmark opt-in, or any other behind the
EmailProvidercontract. React Email templates with first-party open/click tracking, unsubscribe management, and deliverability monitoring — all engine-owned, so they come along with any provider. - Engagement flows back out — Opens, clicks, sends, journey completions, and bucket transitions fan out to PostHog, Segment, Slack, a CRM, or a warehouse via outbound destinations.
Get started in minutes
Scaffold a fresh app, bring the stack up with one command, then run the dev loop:
pnpm dlx create-hogsend@latest my-app
cd my-app
pnpm bootstrap # one command: Docker + .env + Hatchet token + migrate
pnpm dev # HTTP API on http://localhost:3002
pnpm worker:dev # Hatchet worker, in a second terminalpnpm bootstrap is idempotent and safe to re-run. In one pass it:
- Checks Docker is installed and the daemon is running.
- Creates
.envfrom.env.examplewith a freshly generatedBETTER_AUTH_SECRET. - Resolves ports — auto-remaps any busy host port (Postgres, Redis, Hatchet) to the next free one and writes them back to
.env. - Starts containers — TimescaleDB, Redis, and hatchet-lite via Docker Compose.
- Auto-mints your Hatchet token — no bring-your-own-token step locally; it sets
HATCHET_CLIENT_TOKENfor you. (Bring your own only in production / Hatchet Cloud.) - Runs migrations — both the engine track and your client track.
- Mints an ingest-scoped data-plane key — writes a
HOGSEND_API_KEY(hsk_…) so the data API and@hogsend/clientwork out of the box.
Set RESEND_API_KEY in .env before sending real email — everything else is wired for you. Prefer one command? pnpm dlx create-hogsend@latest my-app --yes scaffolds, installs, and bootstraps in a single hands-off step (or pass . instead of a name to scaffold into the current folder). The Hatchet dashboard runs at http://localhost:8888 (admin@example.com / Admin123!!).
To log into Studio, create the first admin first. Public sign-up is disabled, so the first admin is minted from your server — run pnpm studio:admin (which calls hogsend studio admin create), or set STUDIO_ADMIN_EMAIL (+ optional STUDIO_ADMIN_PASSWORD) and the API mints it on boot into an empty user table. There is no web sign-up form.
Quick example
A PostHog user_signed_up event triggers this journey. It sends a welcome email, waits a day, checks if the user tried the core feature, and nudges them if not. Everything imports from @hogsend/engine — the single surface your content depends on:
import { days, defineJourney, sendEmail } from "@hogsend/engine";
export const onboarding = defineJourney({
meta: {
id: "onboarding-welcome",
name: "Onboarding — Welcome Series",
enabled: true,
trigger: { event: "user_signed_up" },
entryLimit: "once",
suppress: days(1),
},
run: async (user, ctx) => {
await sendEmail({
to: user.email,
userId: user.id,
template: "activation/welcome",
subject: "Welcome — let's get you set up",
journeyName: user.journeyName,
});
await ctx.sleep({ duration: days(1), label: "post-welcome" });
const { found } = await ctx.history.hasEvent({
userId: user.id,
event: "feature_used",
});
if (!found) {
await sendEmail({
to: user.email,
userId: user.id,
template: "activation/nudge",
subject: "You haven't tried the key feature yet",
journeyName: user.journeyName,
});
}
},
});You register this journey by adding it to the journeys array your app exports from src/journeys/index.ts — the array you pass to createHogsendClient({ journeys }) and createWorker({ container, journeys }). No YAML, no state machines, no visual canvas. Just TypeScript with if, await, and loops — running as durable tasks that survive restarts and deploys.
What you get
- A versioned engine —
@hogsend/engineis a semver-stable API surface. Upgrade withpnpm up "@hogsend/*"and run migrations. - Code-first journeys —
defineJourney()with durable execution backed by Hatchet. Yourawait ctx.sleep({ duration: days(3) })literally pauses for three days and picks up exactly where it left off — orctx.waitForEvent()pauses until the user actually does something (with a timeout fallback), so journeys react to behavior, not just the clock. - PostHog webhook source — Point your PostHog webhook at your app and events flow through automatically.
- Swappable email delivery — Resend by default, Postmark opt-in, or bring your own behind the
EmailProvidercontract. React Email templates, tracked sends, bounce handling, one-click unsubscribe, preference center — all engine-owned, so they come along with any provider. - A public data plane — Write contacts, events, and transactional emails from your own code with one ingest-scoped key via
POST /v1/eventsor the typed@hogsend/client. - Outbound destinations — Fan the lifecycle event stream out to PostHog, Segment, Slack, a CRM, or a warehouse over the durable webhook spine with
defineDestination(). - Composable conditions — Property checks, event history, email engagement, AND/OR composition. Use them for enrollment guards, exit conditions, and mid-journey branching.
- Two-track migrations — Engine schema ships upstream in
@hogsend/dband gates boot; your own client schema is yours to evolve. Both run from a singlepnpm db:migrate. - Admin API — Contacts, journey control, email metrics, alerting, audit logs. Everything you need to operate without a UI.
- Self-hosted — Deploy to Railway (or anywhere that runs Node.js + Postgres). Your data stays yours.