npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@founderhq/events-node

v0.6.0

Published

FounderHQ server-side event ingest SDK for Node.js

Readme

@founderhq/events-node

Server-side FounderHQ Events capture for Node.js (18+). Use it for server-truth growth events — signups, activations, subscription and payment changes — that browsers cannot be trusted to report.

import { FounderHqNode } from "@founderhq/events-node";

const events = new FounderHqNode("fhq_sk_server_key");

events.capture({
  contact: { externalId: "user_42", email: "[email protected]" },
  event: "subscription.upgraded",
  properties: { plan: "growth", mrr: 99 },
});

await events.shutdown(); // flush on process exit

When server code creates a Stripe/Dodo checkout from browser-provided IDs, use the pure metadata helper:

import { checkoutMetadata } from "@founderhq/events-node";

const metadata = checkoutMetadata({ anonymousId, sessionId });
// { fhq_anonymous_id: "...", fhq_session_id: "..." }

Events are queued in memory and delivered in order as POST /api/events { batch: [...] } with the secret key in Authorization: Bearer. Batches retry with exponential backoff on network or 5xx failures; auth and validation rejections are surfaced through onError and never retried. Each event carries an idempotency key (auto-generated UUID, or pass idempotencyKey) so retried batches never double-count.

The Node SDK uses Events v2's single property namespace: $lib, $lib_version, and $platform are added to properties, and no context object is sent. Node deliberately has no autocapture or client sessions.

Account context is stateless and belongs on each event. Reusing a caller-owned spanId groups related account activity without creating ambient SDK state:

events.capture({
  contact: { externalId: "user_42" },
  account: { key: "workspace_123", spanId: request.accountSpanId },
  event: "report.created",
});

events.upsertAccount({
  key: "workspace_123",
  properties: { plan: "growth" },
});

events.accountMembership({
  account: "workspace_123",
  userId: "user_42",
  state: "left",
  effectiveAt: new Date("2026-08-17T10:30:00Z"),
  idempotencyKey: "membership_user_42_left_2026_08_17",
});

upsertAccount sends a contact-free $groupidentify, so it updates account properties but never implies membership. Omitting account from a later capture is the stateless equivalent of clearing context.

Use accountMembership when your backend knows a user joined or left an account. It keeps later activity from being attributed to an account the user has left. effectiveAt defaults to now, and a stable idempotencyKey makes webhook retries safe.

Use state: "retracted" when the membership was wrong from the start, not when someone departed.

Revenue is sent through the ledger before an analytics event is derived:

await events.captureRevenue({
  idempotencyKey: "webhook_evt_3StableDeliveryId",
  transactionId: "pi_3StableProcessorId",
  transactionRefType: "payment_intent",
  amountMinor: 1299,
  currency: "USD",
  checkoutVisitorId: anonymousId,
});

Use the payment rail's stable transaction ID so retries and cross-adapter copies reconcile to one movement. Amounts are integer minor units (not always cents: currencies can have zero or three minor digits). Ordinary $revenue capture remains blocked; the worker emits it only after ledger reconciliation.

Contacts resolve by externalId, email, or phone — at least one is required. brandId scopes the contact when the key is org-wide. Timestamps default to now and accept Date or ISO strings. The legacy context input is deprecated and promoted into the single property namespace for compatibility.

The package executes every applicable Node fixture from events-core/fixtures/conformance-v2.json exactly with injected clock, UUID, and transport providers, comparing complete requests and normalized outcomes.

Secret keys only: the constructor throws on a publishable (fhq_pk_) key. Use @founderhq/events in browsers instead.

Crawler tracking

AI answer engines and search crawlers do not run JavaScript, so browser analytics never sees them. createCrawlerTracker records those server-side fetches and reports them per brand as AI visibility in FounderHQ. Use the same secret key. Docs: https://www.getfounderhq.com/docs/analytics/ai-visibility

import { createCrawlerTracker } from "@founderhq/events-node/crawlers";

const crawlerTracker = createCrawlerTracker({
  secretKey: process.env.FOUNDERHQ_SECRET_KEY!,
});

The /crawlers subpath is safe to import in Edge middleware: it does not load the Node-only event client or node:crypto.

Next.js middleware (proxy.ts in Next.js 16) must pass NextFetchEvent so delivery stays alive with waitUntil() after middleware returns:

import type { NextFetchEvent, NextRequest } from "next/server";
import { NextResponse } from "next/server";

const crawlerTracker = createCrawlerTracker({
  secretKey: process.env.FOUNDERHQ_SECRET_KEY!,
  proxy: "vercel",
  deliveryMode: "request-scoped",
});

function middleware(_request: NextRequest, _event: NextFetchEvent) {
  return NextResponse.next();
}

export default crawlerTracker.withNextCrawlerTracking(middleware);

Use proxy: "cloudflare" when Cloudflare is the last trusted proxy. For a custom stack, pass one authoritative { ipHeader } policy or a resolveIp callback. Forwarding headers are never trusted as an undifferentiated group.

Express:

// Configure Express `trust proxy` for your actual proxy topology first.
// The tracker uses Express's resolved req.ip.
app.use(crawlerTracker.expressHandler());

Plain Node:

createServer((request, response) => {
  response.end("ok");
  crawlerTracker.trackRequest(request, { statusCode: response.statusCode });
});

trackRequest classifies the user agent in memory, queues the record, and returns. It never throws and never awaits the network. Long-lived Node servers batch with a timer. Request-scoped Next middleware flushes through waitUntil(). Each request body stays below 60 KiB as well as the 50-record cap; a failed batch is dropped, never retried. Static assets, /_next/*, /api/*, /i/*, and non-GET requests are skipped. URLs and user agents are bounded before entering the byte-bounded queue.

| Option | Default | What it does | | --- | --- | --- | | secretKey | required | fhq_sk_ key; the constructor throws on a missing or publishable key | | endpoint | https://app.getfounderhq.com/i/v2/crawlers | Where batches are sent | | enabledCategories | AI_ANSWERS, INDEXING, TRAINING, OTHER | Categories recorded on your server | | proxy | — | One authoritative policy: "vercel", "cloudflare", or { ipHeader, take? } | | resolveIp / resolveUrl | — | Caller-owned resolution for custom proxy or framework behavior | | deliveryMode | long-lived | Use request-scoped with the Next adapter; it disables batching timers | | flushAt | 50 | Queue size that triggers a send (1–50) | | flushIntervalMs | 5000 | Longest wait before a send; 0 disables the timer | | maxQueueSize | 500 | Records kept in memory; oldest are dropped first | | maxQueueBytes | 262144 | Serialized bytes kept in memory; oldest are dropped first | | fetch | global fetch | Custom fetch implementation | | onDrop | — | Called asynchronously with a count when records are dropped |

flush(), shutdown(), and getStats() (queued, dropped, delivered) are available on every tracker. classifyCrawlerUserAgent and CRAWLER_CATALOG are exported for your own checks.

For Next pass-through and rewrite responses, status is recorded as unknown: middleware cannot observe the final page's 404 or 500. Redirect statuses and Express's final response status are recorded when known.