@ingram-tech/nk-billing
v0.3.0
Published
The Ingram billing foundation: composable Stripe primitives (client, customers, prices, currency, checkout, subscriptions, webhooks) plus an injection-based Postgres credit ledger and event-dedup store.
Readme
@ingram-tech/nk-billing
The Ingram billing foundation: composable Stripe primitives every site was re-implementing, plus an optional Postgres credit ledger for usage metering. One account-agnostic toolkit, not a billing framework that owns your flows — you keep your routes, your schema, and your pricing; nk-billing removes the boilerplate in between.
Part of nextkit. Stateless Stripe helpers are the main entry; the database-backed credit ledger lives behind the
/creditssubpath so a subscription-only or wallet-only site never imports a DB concept.
Install
bun add @ingram-tech/nk-billing stripestripe is a direct dependency; bring your own Postgres (pg / nk-db) only if
you use the credit ledger.
Env contract
Single-mode (most sites): STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET.
Dual-mode (a merchant-of-record running test beside live, e.g. cloud):
STRIPE_SECRET_KEY_{TEST,LIVE}, STRIPE_WEBHOOK_SECRET_{TEST,LIVE}.
Optional STRIPE_API_VERSION to pin ahead of an SDK bump.
Nothing hardcodes a price ID — prices resolve at runtime by stable Stripe
lookup_key, so the same code path works in test and live.
The three billing models
Pick the one that matches what you sell — they compose:
- Subscriptions —
createCheckoutSession({ mode: "subscription" }),createPortalSession,summarizeSubscription,fetchSubscriptionSummary(the webhook-lag self-heal read). - Stripe-side wallet (money) —
@ingram-tech/nk-billingreadBalance/debitBalance: the balance lives in Stripe, no local table. - In-app credits (abstract units) —
@ingram-tech/nk-billing/credits: atomicspendCredits/requireCredits,grantCredits,recordSubscriptionStatus, all idempotent; ships its own migration.
Quick start — subscription checkout
import { createCheckoutSession, resolveCurrencyFromHeaders } from "@ingram-tech/nk-billing";
import { headers } from "next/headers";
const url = await createCheckoutSession({
customer: { metadataKey: "acme_org_id", id: orgId },
customerDetails: { name: org.name, email: user.email },
lookupKey: "acme_pro_monthly",
mode: "subscription",
currency: resolveCurrencyFromHeaders(await headers()),
successUrl: `${base}/billing?ok=1`,
cancelUrl: `${base}/billing`,
});
redirect(url);Quick start — webhook (with credit-ledger dedup)
import { readStripeWebhook } from "@ingram-tech/nk-billing";
import { grantCredits, recordSubscriptionStatus } from "@ingram-tech/nk-billing/credits";
export async function POST(request: Request) {
const res = await readStripeWebhook(request, process.env.STRIPE_WEBHOOK_SECRET ?? "");
if (!res.ok) return new Response(res.message, { status: res.status });
// dedupe + apply inside your tenant transaction; retries are no-ops.
await withTenant(orgId, (db) => recordSubscriptionStatus(db, { ... }));
return Response.json({ received: true });
}Credit ledger & tenancy
The ledger takes the DB connection by injection and leaves isolation to you —
wrap each call in withTenant(orgId, db => spendCredits(db, …)) under RLS, or a
plain transaction under app-layer filtering. It owns two tables; apply the
migrations/*.sql files in order (Drizzle-composable fragments) and add your
own RLS policy if your stack uses one. See src/credits.ts.
Two semantics to know before wiring it up:
- Entitlement gates the balance.
spendCreditsrequires an active subscription or a live trial before it looks at credits: a one-time credit pack bought by a tenant with no subscription and an expired trial is not spendable. If your site sells packs standalone, gate pack checkout on entitlement — or passactiveStatusessemantics that match your model. - Webhook ordering. Pass
event.createdaseventCreatedtorecordSubscriptionStatus(and apply0002_billing_status_order.sql) so a delayed, oldercustomer.subscription.updatedcan't overwrite the status a laterdeletedevent already recorded.
Full surface
The per-export reference is the JSDoc on each export — every entry point in
src/index.ts and src/credits.ts is
documented at the definition.
License
MIT © Ingram Technologies
