Hogsend is brand new.Try it
Hogsend
Client-side SDK

Try it live

Sign in with a 6-digit code, fire a first-party event from this page, and watch a journey drop a notification into the bell in the top nav.

This is the same @hogsend/js + @hogsend/react you just installed, running on these docs. Sign in with a 6-digit code (the first one creates your account) and the bell in the top-right becomes your feed. Fire an event below and watch a journey reach it.

Live demoSign in, then fire it

Fire it. Watch your bell.

Sign in below. A 6-digit code arrives by email; first time through, that creates your account. Then fire a real lifecycle event: a journey turns it into a notification in the bell ↗ in the top nav, on the contact behind that account.

not signed in

Checking your session…

Sign in above to fire real lifecycle messages.

  1. 1You fired a first-party event (source: inapp) keyed to your id.
  2. 2The engine resolved your id to a canonical contact key and routed it.
  3. 3A journey triggered on that event, read your name, and called sendFeedItem.
  4. 4It landed in your bell ↗ — open it. Clicking the item fires inapp.item_clicked (and a link.clicked on its tracked CTA) back into the loop.

Unread in your bell: 0. Each item carries a tracked CTA — clicking the row emits inapp.item_clicked and a link.clicked, both real first-party events a journey can react to.

apps/api/src/journeys/demo-inapp.ts
import { days } from "@hogsend/core";
import { defineJourney, sendFeedItem } from "@hogsend/engine";
import { DemoEvents } from "./constants/index.js";

export const demoWelcome = defineJourney({
  meta: {
    id: "demo-welcome",
    name: "Demo — In-app welcome",
    enabled: true,
    trigger: { event: DemoEvents.WELCOME }, // "demo.welcome"
    entryLimit: "unlimited",                // re-fire freely
    suppress: days(0),
  },
  run: async (user) => {
    const n = user.properties.name;
    const name = typeof n === "string" && n ? n : "there";
    const greeting = name === "there" ? "Welcome" : `Welcome, ${name}`;
    await sendFeedItem({
      recipient: { anonymousId: user.id }, // your canonical key
      type: "welcome",
      // Report the event — the site banner already greets by name.
      title: "Your welcome journey just ran ✅",
      body: `${greeting} — you fired demo.welcome, a journey ran, and it dropped into your bell.`,
      actionUrl: "https://hogsend.com/docs/client-side/try",
      journeyStateId: user.stateId,
    });
  },
});

This is the journey that drops the notification into your bell. It reads your name off the event and personalizes the title — it lands on the contact behind your account, the same one the bell reads, so the item is waiting for you on any device you sign in from.

What just happened

Nothing on this page is faked. The button called client.capture("demo.welcome", { name }). That POSTed a first-party event (source: "inapp") carrying your contact key. The engine resolved it, a journey triggered on demo.welcome, read your name off the event, and called sendFeedItem({ recipient: { anonymousId: <your key> } }) — which published to the exact feed the bell polls.

The round-trip uses one identity the whole way, and signing in is what makes that identity yours rather than this browser's. A pk_ publishable key cannot assert who you are, so the browser gets a server-minted userToken: your session calls /api/hogsend-token, which folds { email, userId } onto your contact and signs a token for it. client.getDistinctId() then returns that canonical contact key, and every capture on this page carries it.

Which means the bell follows the account, not the device — sign in on your phone and the same feed is there. It also works backwards: whatever you did on this page before you signed in was recorded against this browser's anonymous id, and identifying adopts that history onto your contact rather than stranding it. Personalization still rides as an event property, so the journey reads user.properties.name and writes it into the notification title.

The item carries an actionUrl, so clicking the row emits inapp.item_clicked — another first-party event, one a journey can trigger on. The loop closes on itself.

// the demo journey, in full — apps/api/src/journeys/demo-inapp.ts
export const demoWelcome = defineJourney({
  meta: {
    id: "demo-welcome",
    trigger: { event: DemoEvents.WELCOME }, // "demo.welcome"
    entryLimit: "unlimited", // re-fire the demo freely
    suppress: days(0),
  },
  run: async (user) => {
    const n = user.properties.name;
    const name = typeof n === "string" && n ? n : "there";
    await sendFeedItem({
      recipient: { anonymousId: user.id }, // re-resolves to your canonical key
      type: "welcome",
      title: `Welcome, ${name} 👋`,
      body: "You fired an event. A journey ran. This is the result.",
      actionUrl: "https://hogsend.com/docs/client-side/try",
      journeyStateId: user.stateId,
    });
  },
});

On this page