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

@12-apps/billing

v1.2.0

Published

Subscription billing: the period arithmetic a renewal anchor survives, the lifecycle a read ages against its own dates, the collection retry policy that never re-presents a card the issuer refused for funds, and the card-on-file surface over a provider va

Downloads

2,267

Readme

@12-apps/billing

Subscription billing for a platform that charges its own customers: the period arithmetic a renewal anchor survives, the lifecycle a read ages against its own dates, the retry policy that decides whether a failed collection is worth another attempt, and the card-on-file surface over a provider vault.

Every number, table and sentence with a commercial opinion in it is required config. The package owns the mechanism and none of the policy — see What stays host-owned.

Two entries, because bundles are physics

| entry | contains | dependencies | |---|---|---| | @12-apps/billing | periods, the lifecycle vocabulary and its ageing, the entitlement seam | none | | @12-apps/billing/server | the retry policy, the cycle collection binding, the card vault, the HTTP surface | @12-apps/payments-backend (peer) |

A browser that needs to say "this account is past due" imports the root entry and pays nothing for the money path. The split is not taste: the payments package is server code, and one shared entry importing it would drag node:crypto into every SPA that ever read a billing status.

The isomorphic half

import { createBillingLifecycle, periodEnd, anchorDayOf } from "@12-apps/billing";

const lifecycle = createBillingLifecycle({ graceDays: 7, suspendAfterDays: 30 });
lifecycle.effectiveStatus(row.status, row, new Date()); // "past_due"

The anchor day is an input. A customer who subscribes on the 31st has no 31st in February; clamping is the only sane answer, but clamping and then advancing from the clamped date walks their billing day permanently earlier (31 Jan → 28 Feb → 28 Mar → …). anchorDayOf(subscribedAt) is read once from the subscribe instant and survives every clamp.

Every read ages the row. A sweep that stops running must not silently hand out free service, so effectiveStatus takes the harsher of (stored, implied-by-dates). The sweep then only ever persists what a read would already have concluded — it is a materialization, not the source of truth.

createBillingLayer is the whole translation into a gate. It turns one subscription row into { planKey, plan, status }, or null when billing has no opinion at all. Both mapping tables are yours; see below.

The server half

import { createChargePolicy, createSubscriptionCollection, createCardVault } from "@12-apps/billing/server";

const policy = createChargePolicy({
  maxAttempts: 4,
  backoffMs: [30 * 60_000, 2 * 60 * 60_000, 8 * 60 * 60_000],
  stopWithoutNewCard: ["INSUFFICIENT_FUNDS"],
});
policy.decideAfterDecline(snapshot, attempts); // { kind: "RETRY", delayMs } | STOP | ALERT | ABORT | DONE

decideAfterError and decideAfterDecline ask three questions in order — did it become a charge, did the provider say no and why, and is another attempt possible at all — against @12-apps/payments-backend's taxonomy rather than a second copy of it, so "is this safe to retry" has exactly one answer.

The vault is two calls and a removal. begin opens a provider session for one subscription; complete is reached from the browser, so its session id is attacker-supplied — what makes it safe is that the reference handed to the adapter comes from the host's own subscription row through the findTarget port, never from the request. forgetAll detaches every pointer the owner holds, not the one on the screen: a card can live at yesterday's acquirer as well as today's.

Nothing here has a parameter a card could travel in. The number goes from the cardholder's keyboard to the provider's SDK to the provider; what crosses these seams is an opaque vault id and the display metadata the provider shared.

Wiring

The package is a @12-apps/wiring producer. It declares http and nothing else, and each absence is deliberate:

| capability | declared | why | |---|---|---| | http | ✅ | the card-on-file surface, four endpoints | | observability | ✅ | namespace billing — the money path is the last place a failure may file nowhere | | db | ❌ | subscriptions, cycles and instruments all carry foreign keys into a host table; a package partial cannot declare a relation into a table it does not own | | permissions | ❌ | who may put a card on file is a role decision, not a permission id this package could name for every host | | notifications | ❌ | the one notice this domain sends is entirely host copy | | mcp | ❌ | a surface that writes a payment instrument stays in a browser, behind a human | | env | ❌ | the package reads no environment variable; everything arrives as config |

import { billingManifest } from "@12-apps/billing/manifest";
import { billingServerManifest } from "@12-apps/billing/manifest/server";
import { createWiringHost } from "@12-apps/wiring/consumer";

const host = createWiringHost({ name: "web", kind: "server", ports: { loggerFor } });
host.adoptServer({
  manifest: billingManifest,
  server: billingServerManifest,
  bindings: { http: { mountPath: "/api/admin/:tenantSlug/subscription", config: { /* … */ } } },
});
const wired = host.assemble();

@12-apps/wiring is a type-only devDependency here; the producer assertions run in this package's own suite, so a malformed manifest fails before any host sees it.

What stays host-owned

Everything below is a required argument, with no default to fall back to silently:

  • the two lifecycle windows — how long before a late payer is penalised, and how long before they are suspended. One platform chases for a week and suspends after a month; the next gives a fortnight and never suspends at all.
  • the retry ladder, the attempt cap, and which declines are chased rather than retried. The payments package refuses these by name — "retry ladders, grace windows, when to ask for a different card, is each host's commercial policy" — and the same boundary holds one layer up.
  • both gate tables — which lifecycle each billing state reaches the gate as, and which states keep the tier they froze.
  • every sentence and every status code the HTTP surface can answer with. A default in the origin platform's language reads as finished to the next platform right up until it reaches a user.
  • the guard. Nothing here authenticates anybody; the host mounts these behind its own resolution and hands the resolved owner in as the actor.
  • the database, through the ports in ./server — the cycle rows, the instrument rows and the subscription lookup.

Each one is checked at construction, not at the renewal that needed it: a mis-stated policy throws BillingConfigError at boot, where an operator is.

Adopting

See ADOPTING.md.