Skip to content
trakoo
Esc
↑↓navigate↵open⌘Jpreview
On this page

EmitKit

Server-only event notifications and activity feeds with channel routing and rich metadata.

EmitKit sends events to named channels with rich metadata, so your team can watch activity feeds and get notified when something important happens.

When to use it

  • Real-time activity feeds and internal dashboards for team visibility.
  • Notifications for high-signal events like signups, payments, or errors.
  • Event logging with user context, routed into channels by topic.

EmitKit is not a product-analytics dashboard. Pair it with a provider like PostHog or Pirsch when you need charts and funnels.

Installation

The EmitKit provider ships in the @trakoo/emitkit package and uses the EmitKit JavaScript SDK.

pnpm add trakoo @trakoo/emitkit @emitkit/js

The provider needs version 3 of the EmitKit SDK.

Client-side usage

EmitKit is server-only — its SDK has no browser package. To capture browser events, use the Proxy to forward them to your server, then send them to EmitKit from there. See Browser events via proxy below.

Server-side usage

import { createServerAnalytics } from "trakoo/server";
import { EmitKitServerProvider } from "@trakoo/emitkit/server";
import { appEvents } from "@/lib/events";

export const serverAnalytics = createServerAnalytics({
  events: appEvents,
  providers: [
    new EmitKitServerProvider({
      apiKey: process.env.EMITKIT_API_KEY!, // starts with 'emitkit_'
      channelName: "general",
    }),
  ],
});

Server provider instances are stateless. Calling identify() updates the remote EmitKit profile, but does not set local identity for later events. Pass the current request’s user id or email with every attributed server event and page view.

EmitKit merges the traits you pass into the profile’s stored properties: new values replace old ones, and properties you leave out are kept.

Track events with properties and optional user context. EmitKit accepts multiple identifiers per user, including an id, email, or username.

await serverAnalytics.track(
  "purchase_completed",
  {
    orderId: "order-123",
    amount: 99.99,
    currency: "USD",
  },
  {
    userId: "user-456",
    user: {
      email: "user@example.com",
      traits: { plan: "pro" },
    },
  },
);

Channel routing

Channels group events into separate streams, similar to Slack channels. The provider resolves the channel for each event in this order:

  1. Per-event override — a __emitkit_channel value in the event properties (highest priority).
  2. Category mapping — the event’s category matched against categoryChannelMap.
  3. Default channel — the channelName option (falls back to 'general').

Map categories to channels so events route automatically:

new EmitKitServerProvider({
  apiKey: process.env.EMITKIT_API_KEY!,
  channelName: "general", // fallback for unmapped categories
  categoryChannelMap: {
    user: "user-activity",
    error: "alerts",
    conversion: "revenue",
  },
});

// Category 'user' → 'user-activity'
await serverAnalytics.track("user_signed_up", { plan: "pro" });

Override the channel for a single event by adding __emitkit_channel. trakoo strips this key before sending, so it never appears in the event metadata.

await serverAnalytics.track("large_payment", {
  amount: 50000,
  __emitkit_channel: "critical-payments",
});

Notifications

The notify option decides whether EmitKit sends a push notification for each event. A single event overrides it with a boolean __emitkit_notify property, so the provider can stay quiet by default and notify only for the events that matter, or the other way round:

new EmitKitServerProvider({
  apiKey: process.env.EMITKIT_API_KEY!,
  notify: false, // feed entries only
});

// This one also sends a push notification.
await serverAnalytics.track("large_payment", {
  amount: 50000,
  __emitkit_notify: true,
});

Only a boolean counts; any other value falls back to notify. trakoo strips __emitkit_notify before sending, like __emitkit_channel. The Better Auth plugin sets both hints on every auth event.

What EmitKit receives

Each tracked event becomes one EmitKit event:

  • Title: the event name in title case, so user_signed_up becomes “User Signed Up”.
  • Icon: chosen from the event category, so feeds stay legible without extra configuration.
  • Description: the event’s description property, or a short default for the built-in categories.
  • Tags: the event category plus a tags property made of strings, without duplicates.
  • Metadata: the event properties without __emitkit_channel and __emitkit_notify, plus category, timestamp, sessionId, and the page, device, UTM, and server context. trakoo removes ip from the device and server context, so the visitor IP address that proxy ingestion records never reaches your feeds. User traits are not attached to events.
  • User: the email from the current call’s user context, then its user id, then the event’s userId.
  • Notification: the event’s __emitkit_notify when it is a boolean, otherwise the notify option.
  • Source: trakoo.

EmitKit stores at most 16 KB per event across all of these fields, metadata included, and rejects a larger event with payload_too_large. The description and tags properties are also part of the metadata, so they count twice toward that limit.

Page views

pageView() sends a silent “Page View” event (notify: false unless its properties set __emitkit_notify, displayed as a message) to the __emitkit_channel in its properties, the channel mapped to the navigation category, or the default channelName, in that order.

EmitKit allows 100 requests per minute per API key by default. Page views forwarded through the proxy can use up the budget your signup and payment events need. If you only want explicit events in EmitKit, exclude page views with routing:

export const serverAnalytics = createServerAnalytics({
  events: appEvents,
  providers: [
    {
      provider: new EmitKitServerProvider({
        apiKey: process.env.EMITKIT_API_KEY!,
      }),
      exclude: ["pageView"],
    },
  ],
});

Errors and rate limits

trakoo turns off the EmitKit SDK’s retries, so a call never waits longer than the timeout. A request that hits the rate limit (HTTP 429), fails, or times out is not delivered. A failed track() is logged by trakoo without affecting other providers. Failed identify() and pageView() calls are logged and do not reject. The provider’s log line names the EmitKit error code, HTTP status, and request id, for example EmitKitError rate_limited 429 request …, so you can tell an invalid API key (unauthorized) from a validation error (validation_error) or a rate limit (rate_limited). A request that got no response is logged as timeout or network_error, without a status.

Browser events via proxy

Send browser events through your own API, then forward them to EmitKit on the server.

import { createClientAnalytics } from "trakoo/client";
import { ProxyProvider } from "trakoo/providers/client";
import { appEvents } from "@/lib/events";

export const analytics = createClientAnalytics({
  events: appEvents,
  providers: [new ProxyProvider({ endpoint: "/api/analytics" })],
});

await analytics.track("button_clicked", { buttonId: "signup" });
import { createServerAnalytics } from "trakoo/server";
import { ingestProxyEvents } from "trakoo/providers/server";
import { EmitKitServerProvider } from "@trakoo/emitkit/server";
import { appEvents } from "@/lib/events";

const serverAnalytics = createServerAnalytics({
  events: appEvents,
  providers: [
    new EmitKitServerProvider({
      apiKey: process.env.EMITKIT_API_KEY!,
      channelName: "user-activity",
    }),
  ],
});

export async function POST(request: Request) {
  await ingestProxyEvents(request, serverAnalytics);
  return new Response("OK");
}

Configuration

PropType
apiKeystring

Your EmitKit API key. Starts with 'emitkit_'.

Typestring
timeout?number

Request timeout in milliseconds. A request that fails or times out is not retried.

Typenumber
Default5000
channelName?string

Default channel when no override or category mapping applies.

Typestring
Defaultgeneral
categoryChannelMap?Record<string, string>

Map event categories to specific channels.

TypeRecord<string, string>
notify?boolean

Send a notification for each event. An event's boolean `__emitkit_notify` property overrides it.

Typeboolean
Defaulttrue
displayAs?'message' | 'notification'

How events appear in EmitKit.

Type'message' | 'notification'
Defaultnotification
debug?boolean

Log debug output for the provider.

Typeboolean
Defaultfalse
enabled?boolean

Enable or disable the provider.

Typeboolean
Defaulttrue

Best practices

  • Route by category with categoryChannelMap and reserve __emitkit_channel for the few events that need a dedicated channel.
  • Name channels by domain — payments, user-signups, team-alerts — so feeds stay scannable.
  • identify() updates a remote profile only; provide the current request’s user id or email on every attributed server event and page view.
  • Include email and username in user traits so events resolve to the same person across identifiers.
  • Exclude pageView unless you want every page view in a feed. Page views count against the same rate limit as your important events.
  • Run EmitKit alongside an analytics provider: EmitKit for feeds and notifications, PostHog or Pirsch for dashboards. See routing to control which events reach each provider.

Resources

Was this page helpful?