
O billing do seu em uma biblioteca.
Defina planos, assinaturas e limites em TypeScript. O dunas cuida das cobranças, renovações e webhooks.
import { createDunas, pg, woovi } from "dunas"
import { free, pro } from "./plans"
export const billing = createDunas({
database: pg(pool),
plans: [free, pro],
providers: [
woovi({ appId: env.WOOVI_APP_ID }),
],
})import { feature, plan } from "dunas"
export const messages = feature({ id: "messages", type: "metered" })
export const free = plan({ id: "free", default: true })
export const pro = plan({
id: "pro",
price: { amountInCents: 4_990, interval: "month" },
includes: [messages({ limit: 5_000 })],
})import { billing } from "./dunas"
export async function POST(request: Request) {
const user = await getUser(request)
const { allowed } = await billing.consume({
customerId: user.id,
featureId: "messages",
})
if (!allowed) return new Response(null, { status: 402 })
return Response.json(await reply(user))
}Cada uso conta. O limite é seu.
O dunas mede cada uso contra o plano do cliente, cobra a assinatura e zera tudo na renovação.
Free · 55 / 100
Do plano à cobrança em três chamadas.
Defina os planos em TypeScript, assine e limite o uso na rota. Webhooks, cron e renovação rodam dentro do dunas.
- 01
Defina os planos
Features, limites e preços ficam num arquivo do seu repositório. Cada id vira tipo.
billing/plans.tsimport { feature, plan } from "dunas" const messages = feature({ id: "messages", type: "metered" }) export const free = plan({ id: "free", default: true, includes: [messages({ limit: 100 })] }) export const pro = plan({ id: "pro", price: { amountInCents: 4_990, interval: "month" }, includes: [messages({ limit: 5_000 })], })→planId: "free" | "pro" - 02
Assine
O cliente assina e recebe a cobrança do gateway escolhido. O navegador espera o pagamento, e todo mês a renovação acontece sozinha.
app/assinar/page.tsxconst result = await dunas.subscribe({ planId: "pro" }) if (result.status === "pending_payment") { showCharge(result.charge) await dunas.waitForPayment({ chargeId: result.charge.id }) }→{ status: "paid", provider: "woovi" } - 03
Limite o uso
Cada uso passa pelo limite do plano antes de ser registrado. A checagem é uma query no seu Postgres.
app/api/chat/route.tsconst { allowed } = await billing.consume({ customerId: user.id, featureId: "messages", }) if (!allowed) return Response.json({ error: "Limite do plano atingido" }, { status: 402 })402Limite do plano atingido
O resto, já resolvido.
Tudo o que dá trabalho em cobrança recorrente, dentro da biblioteca.
await billing.check({
customerId,
featureId: "messages",
})
// { allowed: true, balance: 4812 }Postgres ou PGlite
billing.subscribe({ planId: "pr0" })error Type '"pr0"' is not assignable to type '"free" | "pro"'.
// app/api/dunas/[...path]/route.ts
export const GET = billing.handler
export const POST = billing.handlerimport { All, Controller, Req, Res } from "@nestjs/common"
import type { Request, Response } from "express"
import { toNodeHandler } from "dunas/node"
@Controller("api/dunas")
export class DunasController {
private readonly handler = toNodeHandler(billing.handler)
@All("*path")
handle(@Req() req: Request, @Res() res: Response) {
return this.handler(req, res)
}
}app.all("/api/dunas/*", (c) => billing.handler(c.req.raw))- Cliente do navegador
- Webhook Woovi
- Cron de renovação
createDunas({
// …
on: {
"charge.created": ({ charge }) => sendEmail(charge),
"subscription.canceled": ({ subscription }) => notifyChurn(subscription),
},
})- charge.created
- subscription.activated
- subscription.renewed
- subscription.past_due
- subscription.canceled
Roda no seu app.
É seu.
Código aberto, licença MIT. Inspirado no paykit.
Funciona com o que você já usa
Woovi
AbacatePayPostgres
Better Auth
Next.js
NestJS
Hono
Bun
Deno
TypeScript
