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

@halfagiraf/paykit

v0.1.0

Published

Payments and entitlements for Node apps — Stripe subscriptions and one-off purchases, with the access rules that Stripe deliberately leaves to you.

Readme

Paykit

@halfagiraf/paykit — payments and entitlements for Node applications. Stripe subscriptions and one-off purchases, with the access rules Stripe deliberately leaves to you.

Install it, point your app at it, and one version bump updates every app you own.

Why it exists

Stripe knows about customers, prices and subscriptions. It has no idea what a "plan" means in your application, no notion that Team should satisfy a gate written for Pro, and no concept of someone owning a downloadable report forever while their subscription lapses.

That mapping is identical in every app that sells anything, and writing it again per app is how twenty apps end up with twenty subtly different bugs. This is that layer, as a dependency rather than a template.

Install

npm install @halfagiraf/paykit pg

Published to GitHub Packages, so .npmrc needs:

@halfagiraf:registry=https://npm.pkg.github.com

Use

import { createPaykit } from '@halfagiraf/paykit';
import { registerPaykit, gate } from '@halfagiraf/paykit/fastify';

const paykit = createPaykit({
  pool,                                  // your existing pg Pool
  publicUrl: 'https://yourapp.com',
  catalogue: [
    { key: 'free', name: 'Free', kind: 'subscription', tier: 0,
      priceId: '', priceLabel: '£0', features: [], limits: { projects: 1 } },
    { key: 'pro',  name: 'Pro',  kind: 'subscription', tier: 1,
      priceId: process.env.STRIPE_PRICE_PRO!, priceLabel: '£19/month',
      features: ['Unlimited projects'], limits: { projects: null }, trialDays: 14 },
    { key: 'ebook', name: 'The Ebook', kind: 'one_off',
      priceId: process.env.STRIPE_PRICE_EBOOK!, priceLabel: '£29',
      features: ['PDF download'], repeatable: false },
  ],
});

await paykit.init();   // creates paykit_* tables, warns about misconfiguration

registerPaykit(app, paykit, {
  currentUser: async (req) => myAuth.userFrom(req),   // your auth, not ours
});

// Gate anything. Same call for both kinds — a route never needs to know whether
// the thing was sold as a subscription or bought outright.
app.get('/api/export', async (req, reply) => {
  if (!await gate(paykit, 'pro')(req, reply)) return;
  return doExport();
});

app.get('/api/ebook.pdf', async (req, reply) => {
  if (!await gate(paykit, 'ebook')(req, reply)) return;
  return streamEbook();
});

registerPaykit mounts /api/billing/{catalogue,me,checkout,portal,webhook}.

Environment: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and one STRIPE_PRICE_* per paid product.

The MCP server

An agent can set products up and — more usefully — work out why a payment did nothing.

{
  "mcpServers": {
    "paykit": {
      "command": "npx",
      "args": ["-y", "@halfagiraf/paykit", "paykit-mcp"],
      "env": {
        "STRIPE_SECRET_KEY": "sk_test_...",
        "DATABASE_URL": "postgres://..."
      }
    }
  }
}

Eleven tools, nine of them read-only. paykit_create_product writes to Stripe; paykit_replay_webhook clears one local row and touches Stripe not at all.

| Tool | What it does | |---|---| | paykit_status | Stripe mode, webhook secret, unprocessed count. Start here. | | paykit_diagnose | Compares what Stripe says it sent against what this app recorded | | paykit_list_stripe_prices | Existing products and prices, so nothing is duplicated | | paykit_verify_catalogue | Every priceId checked against the account before a customer finds it | | paykit_create_product | Creates the Stripe product and price, returns the catalogue entry | | paykit_check_entitlement | What a given user actually holds | | paykit_find_customer | Whether this person actually paid, answered from Stripe | | paykit_inspect_stripe_object | One Stripe object, and what Paykit would make of it | | paykit_failed_webhooks | Webhooks that failed, with the error | | paykit_replay_webhook | Clears a failed event so a resend processes cleanly | | paykit_webhook_setup | The endpoint URL and event list to configure |

Live-mode writes are refused unless PAYKIT_MCP_ALLOW_LIVE=yes. An agent with a live key could otherwise create billable objects in a real account on a misunderstanding.

Things worth knowing before taking real money

Stripe is the source of truth. This database caches what Stripe said. Nothing here computes what anyone owes, so a bug gates someone wrongly for a few seconds until the next webhook — it cannot charge them wrongly.

The webhook body must stay raw. The signature covers the exact bytes Stripe sent; re-serialising parsed JSON changes them and the signature never matches. The Fastify adapter registers the right content-type parser so you cannot forget. This is the usual reason webhooks fail in production, and it fails silently.

Webhooks are idempotent by event id, claimed with a UNIQUE insert before any work happens. Stripe retries every non-2xx for three days and duplicates even on success.

One-off purchases outlive subscriptions. Separate tables, not a kind column: a subscription has a status that keeps changing for years, a purchase has none. Cancelling Pro never repossesses something bought outright.

past_due still grants access. The card failed and Stripe is retrying. Cutting someone off the hour their card expires loses more than a few days of service costs.

Partial refunds don't revoke. Only a full refund removes access.

Stripe customer ids are remembered. Paykit writes its own paykit_customers row the first time it creates a customer for one of your users, so a returning customer reuses theirs rather than accumulating one per checkout, and portal() works even if your application stores nothing itself. Pass onCustomerCreated if you want the id mirrored into your own users table as well.

limits is enforced, if you ask. paykit.withinLimit(entitlement, 'projects', count) answers whether one more is allowed; paykit.limitFor gives the number, or null for unlimited. Paykit cannot count your projects, so the count is yours — but the comparison, and the fact that 0 means none rather than unlimited, is not.

Your users stay yours. user_id is TEXT, so a bigint, UUID or Auth0 subject all work. Paykit ships optional paykit_users/paykit_sessions tables for apps with no auth of their own, but references user ids without a foreign key so it sits alongside your schema untouched. All tables are paykit_-prefixed.

Going live: swap to sk_live_, create a separate live webhook endpoint with its own whsec_, and re-create the products — test and live share nothing, including price ids.

Stripe API version

Pinned in src/billing/stripe.ts, in step with the SDK's own default. The types enforce it, so bumping the SDK past a version boundary is a compile error here rather than a runtime surprise in production. Read Stripe's changelog for the intervening versions before changing that line.

Not included, deliberately

Email (Stripe sends its own invoices and receipts), team seat management, and usage-based billing. Each is real work and none is needed for a first payment.

Tests

npm test

241 tests across 8 files, weighted towards the two things expensive to get wrong: webhook signature verification — forged signatures, tampered bodies, replayed timestamps, an unset secret — and the entitlement rules, where treating both kinds of sale alike misbehaves quietly. A purchase must survive a cancelled subscription, a subscription must never imply ownership, and a second purchase of a non-repeatable product must be refused rather than charged.