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

dunasjs

v0.0.2

Published

Billing for Brazil: plans, usage and subscriptions by Pix, Pix Automático or card on Woovi and AbacatePay, inside your app.

Readme

dunas

Billing for Brazil that runs inside your app. Plans, usage limits and subscriptions paid by Pix, Pix Automático or card on top of Woovi and AbacatePay, with one API, your own Postgres and no billing server in between.

const { allowed } = await billing.consume({ customerId: user.id, featureId: "messages" })
if (!allowed) return Response.json({ error: "Limite do plano atingido" }, { status: 402 })

Why

Pix has no card on file. Every month someone has to create a new charge, show the QR code, wait for the payment, extend the period, and cut access when it isn't paid. Pix Automático and cards fix that, but only where the gateway has them turned on. dunas uses them when it can, falls back to a Pix every period when it can't, keeps entitlements in your database so check is one local query, and lets you put Woovi and AbacatePay behind the same API, picking one, rotating between them or failing over when one is down.

Plans

// billing/plans.ts
import { feature, plan } from "dunasjs"

export const messages = feature({ id: "messages", type: "metered" })
export const seats = feature({ id: "seats", type: "metered" })
export const proModels = feature({ id: "pro_models", type: "boolean" })

export const free = plan({
  id: "free",
  default: true,
  includes: [messages({ limit: 100 }), seats({ limit: 1, reset: "never" })],
})

export const pro = plan({
  id: "pro",
  price: { amountInCents: 4_990, interval: "month" },
  includes: [messages({ limit: 5_000 }), seats({ limit: 10, reset: "never" }), proModels()],
})
  • Metered limits reset every month by default (day, week, year or never also work).
  • Plans in the same group (default "base") replace each other. Plans in other groups stack, which is how add-ons work.
  • Customers with no paid plan in a group get that group's default plan.
  • Plan and feature ids are typed everywhere: subscribe({ planId: "pr0" }) does not compile.

Setup

// billing/dunas.ts
import { abacatepay, createDunas, pg, woovi } from "dunasjs"
import { fromBetterAuth } from "dunasjs/better-auth"
import { Pool } from "pg"
import { free, pro } from "./plans"

export const billing = createDunas({
  database: pg(new Pool({ connectionString: process.env.DATABASE_URL })),
  plans: [free, pro],
  providers: [
    woovi({ appId: process.env.WOOVI_APP_ID, methods: ["pix_automatico"] }),
    abacatepay({ apiKey: process.env.ABACATEPAY_API_KEY, webhookSecret: process.env.ABACATEPAY_WEBHOOK_SECRET, methods: ["card"] }),
  ],
  routing: "failover",
  identify: fromBetterAuth(auth),
  on: {
    "charge.created": ({ charge }) => sendPixEmail(charge),
    "subscription.canceled": ({ subscription }) => notifyChurn(subscription),
  },
})

Pix is always on. methods turns on what your gateway account has: "pix_automatico" on Woovi or AbacatePay, "card" on AbacatePay. subscribe only accepts what at least one provider turned on, and TypeScript checks that.

await billing.migrate()
await billing.registerWebhooks({ baseUrl: "https://myapp.com" })

Mount the handler on one route. It serves the browser client, the gateway webhooks and the renewal cron:

// app/api/dunas/[...path]/route.ts (Next.js)
export const GET = billing.handler
export const POST = billing.handler
// Hono, Bun, Deno, anything with Request/Response
app.all("/api/dunas/*", (c) => billing.handler(c.req.raw))
// NestJS: app.module.ts, serves basePath before Nest's body parser, so no rawBody
import { DunasModule } from "dunasjs/nestjs"

@Module({ imports: [DunasModule.forRoot({ billing })] })
export class AppModule {}

Plain Node and Express use the same adapter: createServer(toNodeHandler(billing.handler)), or app.use("/api/dunas", toNodeHandler(billing.handler)) before express.json().

Server API

Customers are created the first time you use their id, on the default plan. Add details whenever you have them (they are sent with Pix charges):

await billing.upsertCustomer({ id: user.id, taxId: "529.982.247-25", phone: "(11) 99999-9999" })
const { subscription, next } = await billing.subscribe({ customerId, planId: "pro" })
// next: { type: "redirect", url, chargeId }   pay on the gateway's page (the default)
// next: { type: "none" }                      free plans, already subscribed, or back to the default plan at period end

await billing.subscribe({ customerId, planId: "pro", method: "pix", checkout: "embedded" })
// next: { type: "pix", brCode, qrCodeImageUrl, expiresAt, chargeId }   show the QR code yourself

await billing.check({ customerId, featureId: "pro_models" })           // { allowed }
await billing.check({ customerId, featureId: "messages", required: 5 }) // { allowed, balance }
await billing.consume({ customerId, featureId: "messages" })          // records only if allowed
await billing.track({ customerId, featureId: "messages", amount: 3 })   // always records
await billing.track({ customerId, featureId: "seats", amount: -1 })     // give a seat back

const { balances, flags, subscriptions } = await billing.getCustomer({ customerId })
balances.messages // { limit, used, remaining, resetsAt }
flags.pro_models  // boolean

await billing.cancel({ customerId, planId: "pro" }) // at period end, or { immediately: true }

Boolean features only take check, and every id is typed from your plans.

Pricing page

listPlans returns your plans with what the button should do for this customer, so the page has no billing logic:

const plans = await billing.listPlans({ customerId })

plans.map(({ id, name, price, action }) => <PlanCard key={id} name={name} price={price} action={action} />)
// action: "current" | "upgrade" | "downgrade" | "subscribe" | "resume" | "pending_payment" | "scheduled"

consume and track take an idempotencyKey so retried requests count once.

Browser client

import { createDunasClient } from "dunasjs/client"
import type { billing } from "@/billing/dunas"

const dunas = createDunasClient<typeof billing>()

const plans = await dunas.listPlans()

const { next } = await dunas.subscribe({ planId: "pro", successUrl: `${location.origin}/thanks` })
if (next.type === "redirect") location.href = next.url

The default is the shortest path: the gateway's page, with Pix Automático when a provider has it. method ("pix", "pix_automatico", "card") and checkout: "embedded" give you more control, like a QR code inside your own page with waitForPayment({ chargeId: next.chargeId }), and are what your coding agent can build on.

The browser never sends a customer id. identify decides who the customer is.

Renewals

Call billing.runRenewals() once an hour, or point your cron at /api/dunas/renewals with Authorization: Bearer $CRON_SECRET (GET for Vercel Cron, or POST). It:

  1. creates the next Pix charge renewDaysBefore days (default 3) before the period ends and fires charge.created, so you can send it to the customer,
  2. extends the period when that charge is paid (subscription.renewed),
  3. marks unpaid subscriptions past_due when the period ends, keeping access for graceDays (default 3),
  4. cancels them after the grace period (subscription.canceled), and the customer falls back to the default plan.

Subscriptions on Pix Automático or card are renewed by the gateway. dunas records each debit from the webhook and extends the period the same way. If the customer revokes Pix Automático or a debit fails, the subscription keeps going on Pix renewals and fires subscription.method_changed.

Access is computed from the dates, so a late cron never gives away extra days.

Routing between gateways

routing: "failover"                                   // first provider, next one if it errors (default)
routing: "round-robin"
routing: { weights: { woovi: 70, abacatepay: 30 } }
routing: ({ amountInCents }) => (amountInCents > 50_000 ? "woovi" : "abacatepay")

A routing function can return a list, which becomes the order dunas tries, and can be async, so it can read your database or remote config before picking.

Webhooks are only a signal: dunas verifies them, then reads the charge back from the gateway before changing anything.

Testing

billing.simulatePayment({ chargeId }) pays a Pix charge in Woovi sandbox or AbacatePay dev mode (embedded checkout only on AbacatePay). Pix Automático and card can't be simulated through the API. For unit tests use PGlite:

import { PGlite } from "@electric-sql/pglite"
import { pglite } from "dunasjs"

const billing = createDunas({ database: pglite(new PGlite()), plans, providers, now: () => clock.now })

Status

Early. Not yet: proration on upgrades, trials, refunds, boleto, changing the payment method of a running subscription.

Community

Questions, ideas or just want to talk? Come say hi on Discord.