Hogsend
Operating

Troubleshooting

Common issues with Hogsend and how to fix them -- events, journeys, emails, Hatchet, database, Redis, and CLI problems.

This page covers the most common issues you will run into when operating Hogsend, organized by symptom. Each section describes what you see, why it happens, and how to fix it.

Events not arriving

You are sending events from PostHog but nothing shows up in Hogsend.

Webhook URL is wrong

Symptom: PostHog's History tab shows delivery failures (4xx or 5xx errors).

Fix: Verify the webhook URL in PostHog > Data > Destinations. It must be:

https://your-hogsend-api.com/v1/webhooks/posthog

Common mistakes:

  • Missing the /v1/webhooks/posthog path
  • Using http:// instead of https://
  • Using localhost -- PostHog's servers cannot reach your local machine

Webhook secret mismatch

Symptom: PostHog's History tab shows 401 Unauthorized responses.

Fix: The x-posthog-webhook-secret header in your PostHog destination must exactly match the POSTHOG_WEBHOOK_SECRET environment variable on your Hogsend API service. Check both values:

# Check what Hogsend expects
# (look at the POSTHOG_WEBHOOK_SECRET env var in Railway or .env)

# Then compare with the header value in PostHog > Data > Destinations > your destination > Headers

The secret must match on both sides — the POSTHOG_WEBHOOK_SECRET your app verifies against and the header value PostHog sends. If they got out of sync (e.g., you regenerated secrets), update both sides to match.

No event matchers configured

Symptom: PostHog shows thousands of deliveries but they are all $pageview, $autocapture, or other internal events. Your journey events are not being sent.

Fix: Add event matchers in the PostHog destination. Without matchers, PostHog sends every event. Add a matcher for each event your journeys care about (e.g., user_signed_up, trial_started). See PostHog Setup for the full walkthrough.

API is not reachable

Symptom: PostHog's History tab shows connection timeouts or DNS errors.

Fix:

  1. Check that the API is running: curl https://your-hogsend-api.com/v1/health
  2. If using Railway, check the service status in the Railway dashboard
  3. If you just deployed, wait for the health check to pass (~2 minutes)
  4. Check that your DNS (Cloudflare CNAME) points to the correct Railway domain

Events arrive but journeys don't trigger

Events are stored in Hogsend (visible via the admin API) but no journey instances are created.

Event name mismatch

Symptom: The event is stored but no journey picks it up.

Fix: The event name must exactly match the trigger.event in your journey definition. Event names are case-sensitive.

# Check what event name was stored
curl -H "Authorization: Bearer $ADMIN_API_KEY" \
  "https://your-hogsend-api.com/v1/admin/events?limit=5"

# Check what event the journey expects
curl -H "Authorization: Bearer $ADMIN_API_KEY" \
  "https://your-hogsend-api.com/v1/admin/journeys/your-journey-id"

Common mismatches: user_signed_up vs user:signed_up, userSignedUp vs user_signed_up. PostHog sends the exact event name as captured in your app.

Journey is not enabled

Symptom: The event name matches but no enrollment happens.

Fix: Check two things:

  1. Runtime toggle -- the journey may have been disabled via the admin API:
curl -H "Authorization: Bearer $ADMIN_API_KEY" \
  "https://your-hogsend-api.com/v1/admin/journeys/your-journey-id"

If enabled: false, re-enable it:

curl -X PATCH "https://your-hogsend-api.com/v1/admin/journeys/your-journey-id" \
  -H "Authorization: Bearer $ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true }'
  1. ENABLED_JOURNEYS env var -- the journey must be in the ENABLED_JOURNEYS list. If set to *, all journeys are enabled. Otherwise, it must be a comma-separated list that includes the journey ID. The same filter governs which journey tasks createWorker registers, so a journey missing from this list will not run even if its trigger event arrives.

  2. Registered in your app's journeys array -- the journey must be exported from your src/journeys/index.ts and passed to both createHogsendClient({ journeys }) and createWorker({ container, journeys }). A journey file that exists but is not in that array is invisible to the engine.

Entry limit reached

Symptom: The user was enrolled before and the journey has entryLimit: "once".

Fix: Check the user's journey history:

curl -H "Authorization: Bearer $ADMIN_API_KEY" \
  "https://your-hogsend-api.com/v1/admin/journeys/your-journey-id/states?userId=user_abc123"

If there is an existing completed or active state and the journey has entryLimit: "once", the user cannot re-enter. To re-enroll, either cancel the existing instance or change the journey's entry limit.

Trigger conditions don't match

Symptom: The event arrives, the journey is enabled, but the trigger.where conditions filter it out.

Fix: Check the journey's trigger conditions and compare against the event properties:

# See the journey's trigger conditions
curl -H "Authorization: Bearer $ADMIN_API_KEY" \
  "https://your-hogsend-api.com/v1/admin/journeys/your-journey-id"

# See the event properties
curl -H "Authorization: Bearer $ADMIN_API_KEY" \
  "https://your-hogsend-api.com/v1/admin/events?userId=user_abc123&event=user_signed_up"

If the journey requires plan == "pro" but the event has plan: "free", the condition fails silently.

User is unsubscribed or suppressed

Symptom: Everything matches but the entry guard rejects the user.

Fix: Check the user's subscription status:

curl -H "Authorization: Bearer $ADMIN_API_KEY" \
  "https://your-hogsend-api.com/v1/admin/contacts/user_abc123/preferences"

If unsubscribedAll: true or suppressed: true, the user will not be enrolled. See Email Operations for how to un-suppress a contact.

Emails not sending

Journeys are running but emails are not being delivered.

Hogsend sends email through a swappable provider — Resend by default. The fixes below name the default Resend env vars; if you swapped in another provider (Postmark, or your own via the EmailProvider contract), check that provider's credentials instead. See Sending Email for the swap details.

Every email arrives at one address

Symptom: Emails send fine, but they all land in a single inbox (yours) with a [TEST → …] subject prefix — real recipients get nothing.

Fix: That's test mode — the engine redirects every send while the sending domain is unverified (or while HOGSEND_TEST_MODE=true). Verify the domain with hogsend domain check and sends go live within a minute; hogsend domain status shows the banner and the reason.

Invalid provider API key

Symptom: Emails fail immediately with a provider API error. Email status is failed.

Fix: On the default Resend provider, verify RESEND_API_KEY is set correctly and starts with re_. Check the Resend dashboard to confirm the key is active and has not been revoked. (RESEND_API_KEY is optional now — a Postmark-only deploy boots without it — but the active provider still needs a valid key for its own service.)

Sending domain not verified

Symptom: Resend returns a domain verification error. Emails fail with status failed.

Fix: The sending domain must be verified with your provider. The quickest path is the CLI: hogsend domain add mysite.com registers it and prints the DNS records formatted for your DNS host (auto-applied on Cloudflare/Vercel when a token is set), then hogsend domain check polls until verified. Or do it by hand in Resend > Domains (SPF, DKIM, DMARC). Until it verifies, test mode keeps sends safe by redirecting them to your own inbox.

If you are using the default noreply@hogsend.com, you need to verify hogsend.com in Resend, which is only possible if you control that domain.

Bounce suppression

Symptom: Emails are not sent to specific contacts. Other contacts receive emails normally.

Fix: The contact may be suppressed due to previous bounces. Check their preferences:

curl -H "Authorization: Bearer $ADMIN_API_KEY" \
  "https://your-hogsend-api.com/v1/admin/contacts/user_abc123/preferences"

If suppressed: true and bounceCount >= 3, the contact was auto-suppressed. See Email Operations for how to un-suppress them.

Provider webhooks not configured

Symptom: Emails show sent status but never progress to delivered, bounced, or complained.

Fix: This means your provider's webhook events are not reaching Hogsend. (Note: opened and clicked are first-party — the engine tracks them itself — so missing opens/clicks point at blocked pixels/links, not the provider webhook.) On the default Resend provider, set up a webhook endpoint in Resend > Webhooks:

  • URL: https://your-hogsend-api.com/v1/webhooks/email/resend (the generic provider route; POST /v1/webhooks/resend is kept as a deprecated alias)
  • Events: email.delivered, email.bounced, email.complained (also email.opened / email.clicked if you want the provider's signals, but first-party tracking is the source of truth)
  • Signing secret: Set this as RESEND_WEBHOOK_SECRET in your Hogsend environment

Other providers post to POST /v1/webhooks/email/:providerId keyed by their own id (e.g. postmark); the payload is normalized to a provider-neutral EmailEvent either way.

Hatchet connection failures

The API or worker cannot connect to the Hatchet engine.

Invalid or missing token

Symptom: API logs show "failed to connect to Hatchet" or gRPC authentication errors. The worker fails to start.

Fix:

  1. Check that HATCHET_CLIENT_TOKEN is set in your environment
  2. Verify the token is valid -- generate a new one from the Hatchet dashboard
  3. For local development, the Hatchet-Lite dashboard is at localhost:8888 (login: admin@example.com / Admin123!!)
  4. For Railway, open the Hatchet-Lite service URL and generate a token from the dashboard

Wrong gRPC endpoint

Symptom: Connection timeouts when the API or worker tries to reach Hatchet.

Fix: Check HATCHET_CLIENT_HOST_PORT:

  • Local development: localhost:7077
  • Railway: Use the internal network address of the Hatchet-Lite service (not the public URL). Format: hatchet-lite.railway.internal:7077

Also check HATCHET_CLIENT_TLS_STRATEGY:

  • Local / Railway internal networking: none
  • Public internet: tls

Hatchet-Lite is not running

Symptom: Connection refused errors.

Fix:

  • Local: Run docker compose up -d and check that the Hatchet-Lite container is healthy: docker compose ps
  • Railway: Check the Hatchet-Lite service in the Railway dashboard. If it crashed, check its logs for database connection issues -- Hatchet-Lite needs its own Postgres instance

Database issues

Migration failures

Symptom: The API fails to deploy on Railway with a migration error in preDeployCommand (pnpm db:migrate), or boots and immediately exit(1)s with "Database schema is out of date".

Fix:

  1. Check the deploy logs in Railway for the specific migration error. pnpm db:migrate runs two tracks — the engine track (bundled in @hogsend/db) first, then your client track (migrations/). Note which track failed.
  2. If a migration failed partway through, you may need to manually fix the database state. Both tracks serialize behind a Postgres advisory lock, so a stuck lock from a killed migrate can block the next attempt.
  3. Verify that DATABASE_URL points to the correct Postgres instance and is reachable
  4. The engine track gates boot — if the API exits at startup with a schema error, the engine schema is behind what the build requires. Confirm preDeployCommand actually ran pnpm db:migrate to completion. As a temporary unblock you can set SKIP_SCHEMA_CHECK=true, but resolve the migration promptly.
# Test the connection locally
psql $DATABASE_URL -c "SELECT 1"

# See which track is behind
curl https://your-hogsend-api.com/v1/health   # check schema.engine / schema.client

Health shows migration_pending

Symptom: GET /v1/health returns status: "migration_pending" instead of healthy.

Fix: One of the two migration tracks is behind the code. Check the schema block:

  • schema.engine.inSync: false -- the engine track is behind. This also blocks boot, so the API may be exiting. Run pnpm db:migrate.
  • schema.client.inSync: false -- your own client migrations are behind (non-fatal, but your src/schema changes are not applied). Run pnpm db:migrate.

A common false alarm is bootstrapping with pnpm db:push or a seed: those create tables without writing a migration ledger row, so the health probe reports inSync: false even though the tables exist — and pnpm db:migrate then fails trying to replay migrations whose objects already exist (e.g. "type already exists"). Prefer pnpm db:migrate from day one.

To recover a db:push-ed database whose schema is already current, stamp the ledger instead of replaying migrations:

cd packages/db && pnpm db:stamp   # records all bundled migrations as applied, runs no DDL

Only run db:stamp when you're confident the schema already matches HEAD — it marks migrations applied without creating their objects. See Deployment.

Connection pool exhaustion

Symptom: Database connection errors under load. Queries timeout or return "too many connections".

Fix:

  1. Check the number of active connections: SELECT count(*) FROM pg_stat_activity;
  2. Railway's managed Postgres has connection limits based on your plan
  3. Consider using a connection pooler (e.g., PgBouncer) for high-throughput deployments
  4. Check for long-running transactions that hold connections open

Redis connection

Symptom: Worker logs show Redis connection errors. PostHog property caching is not working.

Fix:

  1. Verify REDIS_URL is set and the Redis instance is running
  2. Local: Check Docker: docker compose ps -- Redis should be on port 6380
  3. Railway: Check the Redis service status and that it is linked to both API and worker services
  4. Redis is used for caching, not critical state. If Redis is down, the worker will fall back to fetching properties directly from PostHog (slower but functional)

Health check failing

Symptom: Railway shows the API service as unhealthy. The service keeps restarting.

Fix: Hit the health endpoint directly to see what is failing:

curl -v https://your-hogsend-api.com/v1/health

The health check verifies database connectivity and that both migration tracks are in sync. Common causes of failure:

CauseFix
DATABASE_URL not setAdd the variable in Railway and link the Postgres instance
Postgres is still startingWait 1-2 minutes after initial deploy. The health check timeout is 120 seconds.
Engine migrations behindThe boot guard exit(1)s before the server starts. Confirm preDeployCommand ran pnpm db:migrate.
status: migration_pendingA track is behind — see Health shows migration_pending above
Port conflictEnsure PORT is not hardcoded -- Railway sets it automatically

CLI issues

These cover the @hogsend/cli tool. The data commands (doctor, journeys, contacts, stats, events) talk to a running instance's API.

Instance unreachable

Symptom: hogsend doctor reports unreachable, or a command errors with a connection refused / timeout.

Fix:

  1. Confirm the instance is actually running and listening.
  2. Point the CLI at the right base URL — set --url <baseUrl> or HOGSEND_API_URL (defaults to http://localhost:3002).
  3. If it's a remote deploy, make sure the URL is reachable from where you're running the CLI (public domain, not an internal host).

Authentication error

Symptom: an admin command (journeys, contacts, stats, events) returns a 401/403.

Fix: these commands hit /v1/admin/*, which is gated by ADMIN_API_KEY on the server. Pass a matching key with --admin-key <key> or set HOGSEND_ADMIN_KEY / ADMIN_API_KEY in your environment. (doctor reads the unauthenticated health route, so it works without a key.)

Getting more help

If you are stuck on an issue not covered here:

  1. Check the API logs in Railway (or your local terminal) -- most errors include enough context to diagnose the problem
  2. Check the Hatchet dashboard (localhost:8888 locally, or the Hatchet-Lite service URL on Railway) for workflow run details and failure reasons
  3. Use the admin API to inspect events, journey states, and email sends -- see Monitoring for the full set of endpoints
  4. Open an issue on GitHub with the error message and relevant logs

On this page