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

@ezyyeah/affiliate-tracking

v0.1.0

Published

A Convex component for affiliate tracking, attribution, commissions, payouts, and analytics.

Readme

@ezyyeah/affiliate-tracking

Convex Component

Affiliate tracking for Convex: programs, affiliates, campaigns, referral links, click tracking, conversion attribution, commissions, payouts, audit events, cleanup, and daily analytics counters.

This package is component-first. It owns its isolated Convex tables, while your app keeps authentication, authorization, billing, checkout, and user records in the host app. Host functions pass app-resolved user, visitor, admin, and order identifiers into the component as strings.

Install

npm install @ezyyeah/affiliate-tracking

Mount the component in your Convex app config:

import affiliateTracking from "@ezyyeah/affiliate-tracking/convex.config.js";
import { defineApp } from "convex/server";

const app = defineApp();
app.use(affiliateTracking);

export default app;

Run Convex codegen after registering the component:

npx convex dev --typecheck-components

Client Wrapper

Create a small host-side wrapper after codegen has generated components for your app:

import { AffiliateTracking } from "@ezyyeah/affiliate-tracking";
import { components } from "./_generated/api";

export const affiliates = new AffiliateTracking(components.affiliateTracking);

The wrapper is intentionally thin. It gives you typed, ergonomic calls from host Convex functions while leaving auth and business policy in your app.

Usage

Create or update a program from a host mutation after checking that the caller is allowed to manage affiliate settings:

const programId = await affiliates.upsertProgram(ctx, {
  slug: "default",
  name: "Default Program",
  defaultCommissionType: "percentage",
  defaultCommissionValue: 20,
  defaultCurrency: "USD",
  attributionWindowMs: 30 * 24 * 60 * 60 * 1000,
  clickDeduplicationMode: "dedupeKey",
  clickDeduplicationMs: 30 * 60 * 1000,
  conversionLimitMode: "none",
  actorId: adminId,
});

Enroll an affiliate and create a link:

const affiliateId = await affiliates.upsertAffiliate(ctx, {
  programId,
  externalId: userId,
  status: "active",
  actorId: adminId,
});

await affiliates.upsertLink(ctx, {
  programId,
  affiliateId,
  code: "creator-20",
  destinationUrl: "https://convex.dev/pricing",
  actorId: adminId,
});

Track clicks from a host HTTP action and redirect the visitor:

const result = await ctx.runMutation(
  components.affiliateTracking.tracking.trackClick,
  {
    programId,
    code,
    visitorId,
    dedupeKey: `${programId}:${code}:${visitorId}`,
    source: "newsletter",
    referrer: request.headers.get("referer") ?? undefined,
    userAgent: request.headers.get("user-agent") ?? undefined,
  },
);

return Response.redirect(result.destinationUrl, 302);

Record purchases with an idempotency key. The component attributes to an explicit click, an explicit link code, or the latest valid visitor click inside the program attribution window.

await affiliates.recordConversion(ctx, {
  programId,
  externalId: orderId,
  idempotencyKey: `order:${orderId}`,
  visitorId,
  customerId,
  amountCents: 12_500,
  currency: "USD",
});

Approve conversions and create payout batches from approved commissions:

await affiliates.updateConversionStatus(ctx, {
  conversionId,
  status: "approved",
  actorId: financeAdminId,
});

const payoutId = await affiliates.createPayoutBatch(ctx, {
  programId,
  currency: "USD",
  actorId: financeAdminId,
});

Attribution Model

Each program controls how clicks and conversions are deduplicated:

  • attributionWindowMs: how long a click can receive conversion credit.
  • clickDeduplicationMode: "dedupeKey", "visitor", "visitorAndLink", or "none".
  • clickDeduplicationMs: rolling duplicate click window.
  • conversionLimitMode: "none", "visitor", "customer", or "customerOrVisitor".
  • conversionLimitMs: rolling duplicate conversion window for the selected conversion rule.

recordConversion() is retry-safe through idempotency records. Reusing the same idempotencyKey returns the existing conversion result instead of double-counting commissions.

Commission Model

Programs define default commission settings:

  • defaultCommissionType: "percentage" or "fixed".
  • defaultCommissionValue: percentage points or fixed minor units, depending on type.
  • defaultCurrency: the expected commission currency.

Campaigns can override the program commission type and value. Conversions store their calculated commission and can move through pending, approved, rejected, and paid operational states.

Public API

Wrapper methods:

  • upsertProgram(ctx, args)
  • getProgram(ctx, args)
  • upsertAffiliate(ctx, args)
  • upsertCampaign(ctx, args)
  • upsertLink(ctx, args)
  • trackClick(ctx, args)
  • recordConversion(ctx, args)
  • updateConversionStatus(ctx, args)
  • updateCommissionStatus(ctx, args)
  • createPayoutBatch(ctx, args)
  • updatePayoutStatus(ctx, args)
  • getSummary(ctx, args)
  • cleanupExpired(ctx, args)
  • resetProgramData(ctx, args)

Component modules:

  • programs: upsertProgram, getProgram, listPrograms, updateProgramStatus
  • affiliates: upsertAffiliate, getAffiliate, listAffiliates, updateAffiliateStatus
  • campaigns: upsertCampaign, getCampaign, listCampaigns, updateCampaignStatus
  • links: createLink, upsertLink, getLink, resolveLink, listLinks, updateLinkStatus
  • tracking: trackClick, recordConversion, getConversion, listConversions
  • commissions: updateCommissionStatus, updateConversionStatus, listCommissions
  • payouts: createPayoutBatch, updatePayoutStatus, getPayout, listPayableCommissions, listPayouts
  • analytics: getSummary, getDailyBreakdown
  • maintenance: cleanupExpired, resetProgramData

Analytics

Clicks and conversions update daily sharded counters for program, affiliate, campaign, and link scopes through @convex-dev/sharded-counter.

Use getSummary for aggregate dashboard numbers and getDailyBreakdown for bounded date ranges. Analytics read exact per-day counter values, so keep dashboard ranges intentionally bounded.

Operational Notes

  • Host app auth is intentionally not embedded. Check permissions in your app, then pass actorId for auditability.
  • IDs at the component boundary are strings in generated host APIs, as expected for Convex components.
  • All public component functions validate arguments with Convex validators.
  • cleanupExpired removes expired clicks and idempotency records and is safe to call from a cron or admin mutation.
  • resetProgramData is destructive and clears a program's component-owned records and counter totals.
  • Payout creation only includes approved commissions that are not already attached to a payout.

Test Helper

Package consumers can use the test helper with convex-test:

import {
  api,
  initAffiliateTrackingTest,
} from "@ezyyeah/affiliate-tracking/test";

const t = initAffiliateTrackingTest();

const programId = await t.mutation(api.programs.upsertProgram, {
  slug: "default",
  name: "Default Program",
  defaultCommissionType: "percentage",
  defaultCommissionValue: 20,
  defaultCurrency: "USD",
});

Package Exports

  • @ezyyeah/affiliate-tracking
  • @ezyyeah/affiliate-tracking/convex.config.js
  • @ezyyeah/affiliate-tracking/convex.config
  • @ezyyeah/affiliate-tracking/_generated/component.js
  • @ezyyeah/affiliate-tracking/_generated/component
  • @ezyyeah/affiliate-tracking/test

Local Example

The repository includes a runnable Vite + React example app in examples. It demonstrates program creation and selection, affiliate enrollment, link creation, redirect tracking, conversion recording, analytics, approvals, payout creation, paid payout marking, and cleanup.

From the repository root:

pnpm install
pnpm build
pnpm dev

That starts:

  • convex dev --typecheck-components for the example backend in examples/convex
  • Vite on http://127.0.0.1:5173

Convex writes VITE_CONVEX_URL to examples/.env.local. If your deployment site URL cannot be derived from the Convex URL, add:

VITE_CONVEX_SITE_URL=https://your-deployment.convex.site

Local Development

pnpm install
pnpm typecheck
pnpm test
pnpm build
pnpm pack

Run pnpm codegen only after configuring a Convex deployment or inside a host app that has registered the component. Deployment-backed smoke checks still require a Convex project because packaged components need app-side codegen after registration.

References