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

@abdssamie/convex-checkpoints

v0.1.9

Published

A Convex component for event-driven user checkpoints.

Downloads

108

Readme

Convex Checkpoints

Convex Checkpoints lets an app define milestone rules for user activity and run a host-owned callback when a rule is completed.

It fits cases like:

  • send a welcome email 30 minutes after signup
  • grant credits after a user creates 5 posts
  • unlock a badge after profile completion

The component owns rule configuration and per-user factor progress. Your app owns auth, app data, and the callback that performs side effects.

Installation

Install the component in your app's convex/convex.config.ts:

// convex/convex.config.ts
import { defineApp } from "convex/server";
import convexCheckpoints from "@abdssamie/convex-checkpoints/convex.config.js";

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

export default app;

Usage

Rules live in the component. Actions live in your app. The component only tracks progress and tells your callback when a rule is completed.

Configure checkpoints

Create a small app module, usually convex/checkpoints.ts.

// convex/checkpoints.ts
import { components, internal } from "./_generated/api.js";
import { internalMutation, mutation } from "./_generated/server.js";
import { v } from "convex/values";
import { ConvexCheckpoints } from "@abdssamie/convex-checkpoints";

// 1. Create one helper for the installed component.
// `onComplete` points at your app-owned callback below.
export const checkpoints = new ConvexCheckpoints(components.convexCheckpoints, {
  onComplete: internal.checkpoints.onCheckpointComplete,
});

// 2. Register rules that describe completed checkpoints.
// - factor: the activity you track, such as "signup" or "create_post"
// - threshold: the value that completes the rule
// - actionName: the app action your callback should run
export const configureRules = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    await checkpoints.registerRule(ctx, {
      name: "welcome-after-signup",
      factor: "signup",
      threshold: 1,
      actionName: "send_welcome_email",
    });

    await checkpoints.registerRule(ctx, {
      name: "credits-after-five-posts",
      factor: "create_post",
      threshold: 5,
      actionName: "add_credits",
    });

    return null;
  },
});

// 3. Handle completed rules in your app.
// This is a normal Convex function, so it can write tables, call internal
// mutations, or schedule later work.
export const onCheckpointComplete = internalMutation({
  args: {
    userId: v.string(),
    factor: v.string(),
    ruleName: v.string(),
    actionName: v.string(),
    value: v.number(),
    threshold: v.number(),
    completedAt: v.number(),
    occurredAt: v.optional(v.number()),
    payload: v.optional(v.any()),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    switch (args.actionName) {
      case "send_welcome_email":
        await ctx.scheduler.runAfter(30 * 60 * 1000, internal.emails.sendWelcome, {
          userId: args.userId,
        });
        break;
      case "add_credits":
        await ctx.runMutation(internal.credits.add, {
          userId: args.userId,
          amount: 100,
        });
        break;
    }
    return null;
  },
});

// Optional query helpers for dashboards, debug views, and admin tools.
export const { listRules, getProgress, listProgressForUser } =
  checkpoints.api();

Track activity from app mutations

Call trackEvent after the app's real business write succeeds. In this example, creating a post increments the user's "create_post" factor. When the value reaches 5, the "credits-after-five-posts" rule completes and the callback runs with actionName: "add_credits".

// convex/posts.ts
import { mutation } from "./_generated/server.js";
import { checkpoints } from "./checkpoints.js";
import { v } from "convex/values";

export const createPost = mutation({
  args: { body: v.string() },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    // 4. First perform the real app write.
    const postId = await ctx.db.insert("posts", {
      body: args.body,
      userId,
    });

    // Then track the factor that may complete one or more rules.
    await checkpoints.trackEvent(ctx, {
      userId,
      factor: "create_post",
      payload: { postId },
    });

    return postId;
  },
});

When progress crosses a rule threshold, the wrapper schedules onComplete immediately. If an action should happen later, schedule that timing in your handler with Convex's ctx.scheduler.runAfter or ctx.scheduler.runAt, as the welcome email example does.

The component does not store callback references; the app-side wrapper schedules the typed function reference directly.

HTTP

Prefer mutation wrappers for app-driven events. They run inside your app's Convex functions, so you can authenticate the user, validate input, write app data, and track progress in one place.

HTTP ingestion is for server-to-server entry points such as webhooks or trusted backend jobs. Treat it as a public endpoint: anyone who can reach the URL can try to submit an event unless you verify the request.

The HTTP helper exposes the same trackEvent flow through routes. Use it when an external system needs to report a factor without calling your Convex mutations directly.

// convex/http.ts
import { checkpoints } from "./checkpoints.js";

export default checkpoints.http("/checkpoints", {
  token: process.env.CHECKPOINTS_SECRET,
});

This registers both POST /checkpoints and factor-specific routes such as POST /checkpoints/create_post. The factor-specific route uses the path segment as the factor, so the request body only needs the user and payload fields.

curl -X POST "$CONVEX_SITE_URL/checkpoints/create_post" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $CHECKPOINTS_SECRET" \
  -d '{"userId":"user1","postId":"post1"}'

Requests must include Authorization: Bearer <token> when token is set. Keep CHECKPOINTS_SECRET in server-side environment variables only.

Tests

When using convex-test, register the component with the test instance:

import { convexTest } from "convex-test";
import component from "@abdssamie/convex-checkpoints/test";
import schema from "./schema.js";

const modules = import.meta.glob("./**/*.*s");
const t = convexTest(schema, modules);
component.register(t);