Hogsend
Concepts

Philosophy

A versioned engine you consume, with content you own. Get to revenue faster, upgrade without merges, and change anything when you need to.

Get it done. Don't be afraid to tweak.

Hogsend is not a platform you configure through a UI and hope it does what you want. It's a versioned engine you consume as a package, with content — journeys, templates, buckets, lists, destinations, webhook sources — that you author as plain TypeScript and own outright.

The mantra is simple: get it done, then tweak.

Ship the welcome sequence today. See if it moves activation. Adjust the timing tomorrow. Add a branch next week. You're not waiting on a vendor to add a feature or fighting a drag-and-drop canvas to express a conditional. Your journeys are your TypeScript. Change them. And when the framework needs to change, you have a clear, low-cost path for that too.

Engine vs content — the line that makes upgrades cheap

Older lifecycle starters told you to fork a repo and merge upstream forever. Hogsend deliberately doesn't. It draws one hard line:

  • The engine (framework, upstream). @hogsend/engine and the other @hogsend/* packages. The ingestion pipeline, journey runtime, email delivery, tracking, the outbound webhook spine, the HTTP API, and the worker loop. Its public, semver-committed surface is exactly @hogsend/engine's exports. You consume it and upgrade it with pnpm up. You don't edit it.
  • The content (yours, in your repo). Your journeys, email templates, buckets, lists, destinations, webhook sources, custom routes, config, and your own database migrations. You author it and you own it. You inject it into the engine's factories.

The engine never imports your content. Your app's two thin entry files pass content into the engine. This is the real src/index.ts the scaffold emits:

// src/index.ts — yours; wires content into the engine
import { createApp, createHogsendClient } from "@hogsend/engine";
import { buckets } from "./buckets/index.js";
import { destinations } from "./destinations/index.js";
import { templates } from "./emails/index.js";
import { journeys } from "./journeys/index.js";
import { lists } from "./lists/index.js";
import { webhookSources } from "./webhook-sources/index.js";

const client = createHogsendClient({
  journeys,
  buckets,
  lists,
  destinations,
  email: { templates },
});

const app = createApp(client, { webhookSources });

Because you register journeys and sources in your own files — not by editing a shared engine index — there is no merge surface. Upgrading the framework is pnpm up "@hogsend/*" plus a migration run.

You scaffold a fresh app

You start a project with one command:

pnpm dlx create-hogsend@latest my-app

This emits a thin app that pins @hogsend/engine, contains only content, and has the two entry files (src/index.ts, src/worker.ts) already wired. The starter journeys (src/journeys/welcome.ts is the one to edit first), the PostHog webhook source, the email templates, and your Events/Templates constants are copied in as editable starter content — they're yours from minute one, not locked inside the engine.

The scaffolder then offers to set up your local stack. That step (pnpm bootstrap, which you can also re-run by hand — it's idempotent) does everything you'd otherwise do by hand:

  1. Checks Docker is running, then brings up TimescaleDB (Postgres), Redis, and hatchet-lite via docker-compose.yml.
  2. Creates .env from .env.example and generates a fresh BETTER_AUTH_SECRET.
  3. Auto-mints your Hatchet token from inside the hatchet-lite container and writes HATCHET_CLIENT_TOKEN to .env. No dashboard trip — local dev is bring-your-own-token-free. (Production is where you bring your own Hatchet token; see Get a Hatchet token.)
  4. Runs migrations — both the engine track and your client track.
  5. Mints an ingest-scoped data-plane key (hsk_…) and writes HOGSEND_API_KEY to .env, so the @hogsend/client SDK and the hogsend CLI can talk to your instance immediately.
  6. Offers to create your first Studio admin — an interactive, skippable prompt (a no-op in CI / non-TTY, or when STUDIO_ADMIN_EMAIL is already set). Public sign-up is disabled, so the first admin is minted from your server — by this prompt, by hogsend studio admin create, or by the STUDIO_ADMIN_EMAIL env bootstrap on a zero-user database.

Then you run two processes — the api and the worker:

pnpm dev          # HTTP API on http://localhost:3002
pnpm worker:dev   # Hatchet worker (second terminal)

The only thing you bring yourself is an email provider credential — a RESEND_API_KEY for the default provider (or a Postmark token if you opt into that) — when you're ready to send real email. Everything else is set up for you.

The step-by-step walkthrough — scaffold, run against Docker, fire your first journey end-to-end — lives in Installation. This page is the why; that page is the how.

Built to be extended — through injection points

The engine exposes a small set of injection seams. Everything you build flows through them, which is exactly why upgrades stay clean:

  • createHogsendClient({ journeys, buckets?, lists?, destinations?, email?: { provider?, providers?, defaultProvider?, templates? }, analytics? }) — build the client (the shared services bag) from your content. email carries your src/emails templates registry plus a swappable provider (Resend by default; Postmark ships as an opt-in, and you can register more under providers/defaultProvider); analytics is PostHog by default; buckets, lists, and destinations are first-class arrays. An overrides field is a small advanced/test-only escape hatch (replace the whole mailer, swap auth/Hatchet/db).
  • createApp(client, { routes?, middleware?, webhookSources?, onError? }) — mount custom routers, add middleware, serve your webhook sources, replace the error handler.
  • createWorker({ container, journeys, buckets?, extraWorkflows? }) — run the durable worker loop; register extra Hatchet tasks via extraWorkflows (the option is extraWorkflows, not workflows).
  • defineJourney({ meta, run }), defineBucket(), defineList(), defineDestination(), and defineWebhookSource() — the authoring helpers for content.

The architecture is intentionally composable. Journeys are standalone files, webhook sources are standalone files, templates are standalone components in your src/emails/ registry. Add what you need to the arrays (and registry) you inject; delete what you don't.

By you

Every journey is a TypeScript file. Every template is a React component. Every webhook source is a function. There's no proprietary DSL, no locked-in configuration format, no "export your data" problem. You can:

  • Add new event sources (your CRM, your billing system, your support tool) with defineWebhookSource()
  • Write journeys with any logic you can express in TypeScript — loops, external API calls, feature flag checks, A/B tests
  • Fan your lifecycle event stream out to PostHog, Segment, Slack, a CRM, or a warehouse with defineDestination() on the durable webhook spine
  • Extend the admin API with your own endpoints through createApp({ routes })
  • Swap the email provider — Resend is the default, Postmark ships as an installable opt-in (@hogsend/plugin-postmark), and for anything we don't ship you implement the EmailProvider contract yourself with defineEmailProvider and pass it via createHogsendClient({ email: { provider } }). The provider is a dumb delivery+webhook wire; the engine owns render → preferences → tracking → email_sends, so swapping providers never costs you those features

By an AI agent

Hogsend's code-first architecture is intentionally LLM-friendly. Journeys follow a consistent pattern (defineJourney() + metadata + run). Templates follow a consistent pattern (React component + registry entry). Webhook sources follow a consistent pattern (defineWebhookSource() + transform). Everything imports from a single surface: @hogsend/engine. The scaffold even ships authoring skills under .claude/skills so an agent can discover the patterns automatically.

This means an AI agent — Claude, Cursor, Copilot, whatever you use — can:

  • Generate new journeys from a description ("write a journey that nudges users who signed up but haven't invited a teammate after 3 days")
  • Modify existing journeys based on performance data ("the day-2 nudge isn't converting — try day-1 instead and add a discount code")
  • Create email templates from a brief ("write a friendly reminder for users whose trial expires in 3 days")
  • Add webhook sources from API docs ("integrate Stripe webhooks so payment events trigger churn recovery")

The patterns are deliberate. Every journey has the same shape. An agent that's seen one can write the next — and it only ever touches your content, never the engine.

When you need to change the engine itself: Extend → Patch → Eject

Most of the time you'll only ever Extend — and that's the design goal. But when you genuinely need different framework behaviour, Hogsend gives you a ladder with a known upgrade cost at each rung:

  • Extend — use the public injection points above. Zero upgrade cost; clean pnpm up forever. This is the default and covers the overwhelming majority of needs.
  • Patchpnpm patch @hogsend/engine for a small, line-local fix held as a committed .patch. It re-applies on every install and fails loudly if an upgrade moves the patched lines.
  • Ejecthogsend eject <package> copies one package into vendor/ and rewrites just that dependency to a local file link. You maintain a fork of that one package; everything else still pnpm ups.

The full how-to — exact commands, the patch lifecycle, what eject copies and how to un-eject — lives in Upgrading & Customizing. Reach for the smallest rung that does the job.

You're not tied into anything

At the end of the day, your Hogsend app is:

  • A Node.js app running on your infrastructure
  • A Postgres database with your contact and event data
  • TypeScript files in your repo — journeys you wrote, templates you wrote
  • React components for your email templates

There's no vendor lock-in, no proprietary data format, no "please contact sales to export." If you decide tomorrow that Customer.io is the right call, you can query your Postgres database, export your contacts, and take your journey logic with you. The event model is PostHog's event model — it works everywhere.

Opinionated where it matters

Hogsend makes choices so you don't have to:

  • PostHog as the standard event source — the webhook source ships as starter content and person properties are fetched automatically, but it's not required to boot; events can also arrive from your own app, any defineWebhookSource(), or the public data plane
  • Resend as the default email provider — fast delivery, developer-friendly API, great deliverability defaults. The provider is swappable: Postmark ships as an opt-in, and anything else slots in behind the EmailProvider contract
  • Hatchet for durability — sleeps that survive deploys, automatic retries, event routing
  • Railway as a paved deploy path — your scaffolded app runs as two services (api + worker) from a single repo, but the image runs anywhere — see Deployment
  • Code over config — TypeScript journeys, not YAML. React templates, not a template editor.
  • Consume, don't fork — the engine is a versioned package, upgraded with pnpm up, not a codebase you merge

These are defaults, not constraints. You can swap most of them out through injection points — a different email provider, a different analytics backend, extra event sources — but you probably won't need to until you've outgrown what this stack can do, which is quite a lot.

The happy path

Hogsend is built for a specific outcome: you ship lifecycle marketing that generates revenue, and you do it this week, not next quarter.

The typical path:

  1. Day 1 — Scaffold with create-hogsend, run pnpm bootstrap, connect PostHog, deploy the included welcome journey
  2. Week 1 — Add payment-failure recovery and trial-expiring nudges. Watch open rates.
  3. Week 2 — Tune timing based on data. Add a dormancy reactivation flow.
  4. Month 1 — You have 4-5 journeys running, measurably improving activation and reducing churn
  5. Month 3+ — Decide if you need more (advanced segmentation, more channels) and either extend the engine or migrate to a bigger platform with clean data. Keep upgrading the engine with pnpm up the whole time.

The point isn't to be the last lifecycle tool you ever use. It's to be the one that gets you from "we should really be sending lifecycle emails" to "we are, and they're working" in the shortest possible time — and that keeps upgrading without ever forcing you to merge a fork.

Next steps

On this page