Hogsend is brand new.Try it
Hogsend
Components

The components, wired to your journeys

The in-app feed, bell, survey card and preference center are real @hogsend/react + @hogsend/js components — drop them into your app. Email, Discord, Telegram and PostHog ride the same journeys and one identity. One theming surface across all of it.

In-app

Drop them into your app

Knock-grade in-app messaging as React components, on a zero-dependency browser client. Every interaction is a first-party event your journeys trigger on — inapp.item_clicked, inapp.preference_changed, a survey answer read by ctx.waitForEvent — not just analytics.

One provider, then import what you need

The kit is the feed, the bell, the in-feed survey card and the preference center — plus a banner and a toast. Import the ones you need; a hooks-only import pulls no CSS. The bell ↗ in this nav is the same NotificationBell, live.

app/inbox.tsx
import {
  HogsendProvider,
  NotificationBell,
  NotificationFeed,
} from "@hogsend/react";
import { PreferenceCenter } from "@hogsend/react/preferences";
import "@hogsend/react/styles.css";

// One provider, one publishable key — every component reads from it.
export function Inbox() {
  return (
    <HogsendProvider apiUrl={API_URL} publishableKey="pk_live_…">
      <NotificationBell onClick={toggle} />
      <NotificationFeed feedId="in_app" />
      <PreferenceCenter />
    </HogsendProvider>
  );
}

Fires demo.survey. The NPS card lands in the feed below — answer it and a journey drops the thank-you item.

Notifications
    src/journeys/demo-survey.ts
    import { days } from "@hogsend/core";
    import { defineJourney, sendFeedItem, sendSurvey } from "@hogsend/engine";
    import { Events } from "./constants/index.js";
    
    // demo.survey → drop an in-app NPS card the visitor answers in their bell.
    export const demoSurvey = defineJourney({
      meta: {
        id: "demo-survey",
        name: "Demo — In-app survey",
        enabled: true,
        trigger: { event: Events.DEMO_SURVEY }, // "demo.survey"
        entryLimit: "unlimited",                // re-fire freely
        suppress: days(0),
      },
      run: async (user) => {
        await sendSurvey({
          recipient: { anonymousId: user.id }, // your canonical key
          event: Events.DEMO_NPS_SUBMITTED,    // "demo.nps_submitted"
          mode: "nps",
          property: "score",                   // the answer rides here
          prompt: "How likely are you to recommend Hogsend?",
          title: "Quick question 👇",
          minLabel: "Not likely",
          maxLabel: "Very likely",
        });
        // sendSurvey has NO journeyStateId option — replay-safety is auto-keyed off
        // the Hatchet run anchor inside sendFeedItem.
      },
    });
    
    // demo.nps_submitted → a thank-you item echoing the score (closes the loop).
    export const demoNpsAnswered = defineJourney({
      meta: {
        id: "demo-nps-answered",
        name: "Demo — NPS answered → thank-you",
        enabled: true,
        trigger: { event: Events.DEMO_NPS_SUBMITTED }, // "demo.nps_submitted"
        entryLimit: "unlimited",
        suppress: days(0),
      },
      run: async (user) => {
        const raw = user.properties.score; // SurveyBlockView captured it under "score"
        const score =
          typeof raw === "number" || typeof raw === "string" ? String(raw) : "?";
        await sendFeedItem({
          recipient: { anonymousId: user.id },
          type: "survey-thanks",
          title: `Thanks — you scored ${score} 🙏`,
          body: "You answered an in-app survey. That emitted demo.nps_submitted onto the spine — a journey read your score and dropped this.",
          actionUrl: "https://hogsend.com/docs/client-side/survey",
          journeyStateId: user.stateId, // sendFeedItem DOES accept this
        });
      },
    });
    Email preferences
    Loading…

    Toasts, re-skinned with tokens

    Toast and ToastContainer are in the kit too. Pop one and recolor it live — the same --hs-* token surface that themes the feed, the bell and the survey card.

    Skin

    Fires useToast().show() — it renders bottom-right via <ToastContainer>, re-skinned by the --hs-* tokens you pick. No CVA, no design lock-in.

    Theme with tokens

    Re-skin every component from one --hs-* block. No CVA, no Tailwind required — the whole kit reads CSS variables.

    Style any slot

    Per-slot classNames and data-* state attributes on every element, so it matches your design system exactly.

    Or replace it wholesale

    renderItem, renderHeader and asChild hand you the markup. The closed-loop events still fire, so a custom UI can't opt out.

    Email

    Email that asks, and listens

    Templates are React components in your repo. The links inside are events, the opens and clicks are first-party, and the provider is your own.

    The click is the answer

    EmailAction renders a link whose click means something. A yes/no is two of them, an NPS is eleven — each answer fires a real event with its payload, and the journey branches on it. First click per send wins; scanner bursts are filtered before anything is recorded.

    NPS & yes/noAnswer = eventScanner-safe
    The email
    Onboarding
    Ready for your onboarding call?
    We’ll set you up in 20 minutes.
    Yes, book it
    Not yet
    The journey reacts
    src/journeys/onboarding.ts
    const answer = await ctx.waitForEvent({
    event: "onboarding.call_answered",
    timeout: days(3),
    });
    if (answer.properties?.answer === "yes") {
    await sendEmail({ template: "booking-link" });
    }
    ctx.waitForEventresolvedanswer = "yes"
    Connectors

    The channels they already live in

    Link a member's Discord or Telegram to their contact, and their activity there becomes a journey trigger — a reaction, a server join, a message — on the same identity as their email and product events.

    Discord

    /link confirms an email — folds a member's Discord and email onto one contact and grants the verified role. Reactions, joins and messages trigger journeys; a journey can grant roles and post back.

    Discord integration →

    Telegram

    A t.me/<bot>?start= link or /link binds Telegram to a contact. Messages trigger journeys, and a journey can reply in the same chat.

    Telegram integration →
    PostHog

    PostHog, both directions

    hogsend connect posthog opens one browser consent. Person reads resolve timezones and property conditions; every email and lifecycle event fans back into PostHog as a captured event; and cross-channel ids — Discord, Telegram, email — fold into one person.

    src/journeys/feedback-nps.ts
    export const survey = defineJourney({
    meta: { trigger: { event: Events.USER_CREATED } },
    run: async (user, ctx) => {
    await sendEmail({ template: "nps-survey" });
    const answer = await ctx.waitForEvent({
    event: Events.NPS_SUBMITTED,
    timeout: days(3),
    });
    const score = answer.properties?.score;
    getPostHog()?.identify(user.id, {
    nps_score: score,
    });
    },
    });
    The run
    eventuser.created · doug@hogsend.comenrolled
    send
    Quick question — how are we doing?deliveredopened
    emit
    PostHog
    wait
    nps.submitted · timeout 3dwaiting.
    score: 9
    identify
    PostHog
    Get started

    Every component, one scaffold

    The scaffold puts the components, the journeys, and the email templates in a repo you own — wired to one engine. Import the first component in your editor.

    pnpm dlx create-hogsend@latest my-app

    Free to self-host · One scaffold command · No per-contact billing