orvacon

subkit

Recurring charges for orvacon — a stateless subscription state machine with renewal, dunning, and cancel.

@orvacon/subkit owns the subscription state machine — when a subscription is due, charging the saved card, advancing the period, and routing a failed renewal through dunning — while your database owns the rows and your cron drives the loop. It holds no money and no store: each charge runs card → your gateway, and every state change comes back for you to persist.

bun add @orvacon/subkit
import { subkit, days } from "@orvacon/subkit";

const subs = subkit({ orva, retries: [days(1), days(3), days(7)] });

let sub = subs.start({
  id: "sub_42",
  amount: money(5_000, "TRY"),
  card, // a vaultkit CardOnFile, or any { token, userKey? }
  interval: { unit: "month", count: 1 },
  firstChargeAt: now,
});

// your cron:
if (subs.due(sub, now)) {
  const { subscription } = await subs.run(sub, now);
  await myDb.subscriptions.update(sub.id, subscription); // active | past_due | canceled
}

run charges and folds the result into the next state: a success advances currentPeriodEnd by one interval; a failure opens or advances dunning (past_due while retries remain, canceled once spent). cancel / pause / resume round out the lifecycle. Renewals are calendar-correct — a monthly subscription anchored on Jan 31 renews Feb 28, not skipping into March. The per-charge idempotency key is derived from the subscription id, period, and dunning attempt, so a cron that fires twice never double charges.

Scope is a recurring charge, not a billing platform. subkit charges the same amount each period and handles renewal, dunning, and cancellation. Proration, plan changes mid-cycle, metered usage, tax, and invoicing live in your code above it — subkit stays the small, correct core they build on. It composes the kits below rather than re-implementing them.