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

better-auth-asaas

v0.1.6

Published

Better Auth plugin for Asaas payment gateway integration

Downloads

26

Readme

better-auth-asaas

A Better Auth plugin that integrates with Asaas, the Brazilian payment gateway.

Currency: Asaas operates exclusively in BRL (Brazilian Reais, R$). All value fields throughout this plugin are in BRL.

Features

  • 🚀 Auto-creates an Asaas customer when a user signs up
  • 🔗 Links asaasCustomerId to the user record automatically
  • 💳 Subscription management — create & cancel subscriptions (Boleto, Pix, Credit Card)
  • 🔔 Webhook handler — keeps local subscription status in sync with Asaas events
  • 📬 Billing event hooks — disable ALL Asaas-side notifications (Email, SMS, WhatsApp, Voice robot, Correios) and handle everything yourself via any provider (Resend, SendGrid, Nodemailer…)
  • 🔒 Session-protected endpoints — all billing actions require an authenticated session
  • 🏖️ Sandbox support — test safely with your Asaas sandbox account

Installation

npm install better-auth-asaas

Setup

Server

// auth.ts
import { betterAuth } from "better-auth";
import { asaas } from "better-auth-asaas";

export const auth = betterAuth({
  plugins: [
    asaas({
      apiKey: process.env.ASAAS_API_KEY!,
      sandbox: process.env.NODE_ENV !== "production",
      // Optional: group all customers under a named segment in Asaas
      // customerGroupName: "my-saas-prod",
    }),
  ],
});

Client

// auth-client.ts
import { createAuthClient } from "better-auth/client";
import { asaasClient } from "better-auth-asaas/client";

export const authClient = createAuthClient({
  plugins: [asaasClient()],
});

Database migration

Run Better Auth's migration to add the asaasCustomerId column to the user table and create the asaasSubscription and asaasPayment tables:

npx better-auth migrate

Using Drizzle or Prisma?

# Prisma
npx prisma migrate dev

# Drizzle
npx drizzle-kit push

Using ZenStack?

The plugin is ORM-agnostic — it goes through Better Auth's adapter API, so it works with any ORM Better Auth supports. The only requirement is that the tables exist in your database.

Add these models to your ZenStack schema file and run your migration:

model User {
  // ... your existing fields
  asaasCustomerId      String?              @unique
  asaasSubscriptions   AsaasSubscription[]
  asaasPayments        AsaasPayment[]
}

model AsaasSubscription {
  id                String   @id @default(cuid())
  userId            String
  user              User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  asaasId           String   @unique
  status            String
  billingType       String
  value             Float
  nextDueDate       String
  description       String?
  externalReference String?
  trialEndsAt       String?
  createdAt         DateTime @default(now())
  updatedAt         DateTime @updatedAt

  // Restrict access to the owner — adjust policies to fit your app
  @@allow('all', auth().id == userId)
  @@map("asaasSubscription")
}

model AsaasPayment {
  id                String   @id @default(cuid())
  userId            String
  user              User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  asaasId           String   @unique
  status            String
  billingType       String
  value             Float
  dueDate           String
  description       String?
  invoiceUrl        String?
  bankSlipUrl       String?
  pixQrCodeId       String?
  externalReference String?
  createdAt         DateTime @default(now())
  updatedAt         DateTime @updatedAt

  @@allow('all', auth().id == userId)
  @@map("asaasPayment")
}

Then run your ZenStack/Prisma migration:

zenstack generate && npx prisma migrate dev

Usage

Get Asaas customer

const { data } = await authClient.asaas.getCustomer();
console.log(data.customer); // AsaasCustomer | null

Create a subscription with a free trial

Pass trialDays instead of nextDueDate — the plugin computes the billing start date automatically. The subscription is ACTIVE immediately; Asaas simply won't generate the first charge until the trial ends.

const { data } = await authClient.asaas.subscriptionCreate({
  billingType: "PIX",
  value: 49.9,
  trialDays: 14,       // free for 14 days, then billed monthly
  cycle: "MONTHLY",
  description: "Pro plan (with trial)",
});
// data.trialEndsAt — date billing kicks in

Create a subscription

Currency: All value fields are in BRL (Brazilian Reais, R$). Asaas is a Brazilian payment gateway and only operates in BRL.

const { data } = await authClient.asaas.subscriptionCreate({
  billingType: "PIX",
  value: 49.9,
  nextDueDate: "2026-03-01",
  cycle: "MONTHLY",
  description: "Pro plan",
});

Cancel a subscription

await authClient.asaas.subscriptionCancel({
  subscriptionId: "sub_xxx",
});

Webhook

The plugin registers a webhook endpoint at:

POST /api/auth/asaas/webhook

This is the URL Asaas will call to notify your app of billing events (payment confirmed, subscription canceled, etc.). You need to register it in your Asaas dashboard so events flow through.

Step 1 — Register in Asaas dashboard

Go to Asaas → Configurações → Notificações → Webhooks and add:

https://your-app.com/api/auth/asaas/webhook

Also set a Token de autenticação — Asaas will send it as the asaas-access-token header on every request. Pass it to the plugin as webhookSecret and the plugin will automatically reject any request that doesn't match:

asaas({
  apiKey: process.env.ASAAS_API_KEY!,
  webhookSecret: process.env.ASAAS_WEBHOOK_SECRET!, // must match what you set in Asaas dashboard
})

⚠️ Without a webhookSecret, anyone who knows your webhook URL can send fake events. Always set this in production.

For local development use a tunnel like ngrok or localtunnel:

ngrok http 3000
# then register: https://abc123.ngrok.io/api/auth/asaas/webhook

Step 2 — Make sure the route is publicly accessible

The webhook URL must be reachable by Asaas without authentication. Better Auth exposes all plugin endpoints under /api/auth/... — make sure your framework isn't adding auth middleware to that path.

Step 3 — Handle events via the events option

asaas({
  apiKey: process.env.ASAAS_API_KEY!,
  events: {
    onPaymentConfirmed: async ({ payment }) => {
      // grant access, send receipt, etc.
    },
    onSubscriptionCanceled: async ({ subscription }) => {
      // revoke access, trigger win-back email, etc.
    },
  },
})

Without any events handlers the webhook still works — it silently keeps your local asaasSubscription and asaasPayment table statuses in sync with Asaas.

Billing event handlers

By default, all Asaas-side customer notifications are disabled on every customer created by this plugin — that includes Email, SMS, WhatsApp, Voice robot (robô de voz), and Correios. This saves you the per-notification fees Asaas charges for each channel.

You receive the raw webhook events via the events option and decide how to act on them (send emails, pause access, trigger CRM flows, etc.).

import { Resend } from "resend"; // or any provider

const resend = new Resend(process.env.RESEND_API_KEY);

export const auth = betterAuth({
  plugins: [
    asaas({
      apiKey: process.env.ASAAS_API_KEY!,
      sandbox: true,
      events: {
        // Set to false to re-enable Asaas-side notifications for all channels
        // disableAsaasNotifications: false,

        // ── Payment lifecycle ────────────────────────────────────────────────

        // New charge created. For PIX: pixQrCode (encodedImage + payload) is included.
        onPaymentCreated: async ({ payment, pixQrCode }) => {
          await resend.emails.send({
            from: "[email protected]",
            to: "[email protected]", // look up from your DB using payment.customer
            subject: "New charge created",
            html: pixQrCode
              ? `<p>R$ ${payment!.value} — scan the PIX QR code below.</p>
                 <img src="data:image/png;base64,${pixQrCode.encodedImage}" />`
              : `<p>A new charge of R$ ${payment!.value} was created.</p>`,
          });
        },

        // ~10 days before due date. PIX QR code included if applicable.
        onPaymentDueSoon: async ({ payment }) => { /* send reminder */ },

        // Due today and still unpaid
        onPaymentDue: async ({ payment }) => { /* send urgent reminder */ },

        // Overdue — fires on due date +1 day, then every 7 days
        onPaymentOverdue: async ({ payment }) => { /* send dunning email, pause access */ },

        // Payment confirmed / received
        onPaymentConfirmed: async ({ payment }) => { /* send receipt, restore access */ },

        // Payment refunded
        onPaymentRefunded: async ({ payment }) => { /* send refund confirmation */ },

        // Chargeback requested or under dispute
        onPaymentChargeback: async ({ payment }) => { /* alert team, pause account */ },

        // ── Subscription lifecycle ───────────────────────────────────────────

        // New subscription created — great for onboarding / welcome sequence
        onSubscriptionCreated: async ({ subscription }) => { /* send welcome email */ },

        // Subscription renewed (new billing cycle auto-generated by Asaas)
        onSubscriptionRenewed: async ({ subscription }) => { /* send renewal confirmation */ },

        // Subscription canceled — trigger win-back campaign
        onSubscriptionCanceled: async ({ subscription }) => { /* send cancellation email, revoke access */ },

        // Catch-all for any Asaas event not handled above
        onOtherEvent: async ({ event }) => {
          console.log("Unhandled Asaas event:", event);
        },
      },
    }),
  ],
});

Asaas webhook events mapped: | Handler | Asaas event(s) | When it fires | |---|---|---| | onPaymentCreated | PAYMENT_CREATED | New charge generated (one-time or subscription cycle) | | onPaymentDueSoon | PAYMENT_DUE_DATE_REMINDER | ~10 days before due date | | onPaymentDue | PAYMENT_OVERDUE (due today) | Due today, still unpaid | | onPaymentOverdue | PAYMENT_OVERDUE (past due) | Overdue — repeats every 7 days | | onPaymentConfirmed | PAYMENT_CONFIRMED / PAYMENT_RECEIVED | Payment confirmed | | onPaymentRefunded | PAYMENT_REFUNDED / PAYMENT_PARTIALLY_REFUNDED | Refund issued | | onPaymentChargeback | PAYMENT_CHARGEBACK_* | Chargeback requested or in dispute | | onSubscriptionCreated | SUBSCRIPTION_CREATED | New subscription activated | | onSubscriptionRenewed | SUBSCRIPTION_RENEWED | New billing cycle started | | onSubscriptionCanceled | SUBSCRIPTION_DELETED | Subscription canceled |

Plugin options

| Option | Type | Required | Description | |---|---|---|---| | apiKey | string | ✅ | Your Asaas API key | | sandbox | boolean | | Use sandbox environment (default: true) | | userAgent | string | | Value sent as User-Agent header (default: "better-auth-asaas"). Mandatory for Asaas accounts created after 06/11/2024. | | disableAutoCreateCustomer | boolean | | Skip auto-creating customer on sign-up | | onCustomerCreated | (customerId, userId) => void | | Callback after customer is created | | webhookSecret | string | | Token set in Asaas dashboard (Token de autenticação). Plugin validates the asaas-access-token header and rejects unmatched requests with 401. | | customerGroupName | string | | Asaas group name assigned to every customer on creation. Useful for multi-tenancy — customers with the same email stay separated per tenant/site. Asaas creates the group automatically if it doesn't exist. | | events | AsaasEventHandlers | | Billing event handlers — see Billing event handlers | | events.disableAsaasNotifications | boolean | | Disable ALL Asaas-side notifications — Email, SMS, WhatsApp, Voice, Correios (default: true) |

License

MIT