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

@alexasomba/better-auth-paystack

v1.2.0

Published

Production-ready Paystack billing plugin for Better Auth. Supports subscriptions, one-time payments, organization billing, secure webhooks and more

Readme

Better Auth Paystack Plugin

A TypeScript-first plugin that integrates Paystack into Better Auth, providing a production-ready billing system with support for subscriptions (native & local), one-time payments, trials, organization billing, and secure webhooks.

Live Demo (Tanstack Start) | Source Code

Features

  • [x] Billing Patterns: Support for Paystack-native plans, local-managed subscriptions, and one-time payments (products/amounts).
  • [x] Auto Customer Creation: Optional Paystack customer creation on user sign up or organization creation.
  • [x] Trial Management: Configurable trial periods with built-in abuse prevention logic.
  • [x] Organization Billing: Associate subscriptions with organizations and authorize access via roles.
  • [x] Enforced Limits & Seats: Automatic enforcement of member seat upgrades and resource limits (teams).
  • [x] Scheduled Changes: Defer subscription updates or cancellations to the end of the billing cycle.
  • [x] Proration: Immediate mid-cycle prorated charges for seat and plan upgrades.
  • [x] Popup Modal Flow: Optional support for Paystack's inline checkout experience via @alexasomba/paystack-browser.
  • [x] Webhook Security: Pre-configured signature verification (HMAC-SHA512).
  • [x] Transaction History: Built-in support for listing and viewing local transaction records.

Quick Start

1. Install Plugin & SDKs

npm install better-auth @alexasomba/better-auth-paystack @alexasomba/paystack-node

Optional: Browser SDK (for Popup Modals)

npm install @alexasomba/paystack-browser

2. Configure Environment Variables

PAYSTACK_SECRET_KEY=sk_test_...
PAYSTACK_WEBHOOK_SECRET=sk_test_... # Usually same as your paystack secret key
BETTER_AUTH_SECRET=...
BETTER_AUTH_URL=http://localhost:8787

3. Setup Server Plugin

import { betterAuth } from "better-auth";
import { paystack } from "@alexasomba/better-auth-paystack";
import { createPaystack } from "@alexasomba/paystack-node";

const paystackClient = createPaystack({
  secretKey: process.env.PAYSTACK_SECRET_KEY!,
});

export const auth = betterAuth({
  plugins: [
    paystack({
      paystackClient,
      paystackWebhookSecret: process.env.PAYSTACK_WEBHOOK_SECRET!,
      createCustomerOnSignUp: true,
      subscription: {
        enabled: true,
        plans: [
          {
            name: "pro",
            planCode: "PLN_pro_123", // Native: Managed by Paystack
            freeTrial: { days: 14 },
            limits: { teams: 5, seats: 10 }, // Custom resource & member limits
          },
          {
            name: "starter",
            amount: 50000, // Local: Managed by your app (500 NGN)
            currency: "NGN",
            interval: "monthly",
          },
        ],
      },
      products: {
        products: [{ name: "credits_50", amount: 200000, currency: "NGN" }],
      },
    }),
  ],
});

4. Configure Client Plugin

import { createAuthClient } from "better-auth/client";
import { paystackClient } from "@alexasomba/better-auth-paystack/client";

export const client = createAuthClient({
  plugins: [paystackClient({ subscription: true })],
});

5. Migrate Database Schema

npx better-auth migrate

Billing Patterns

1. Subscriptions

Native (Recommended)

Use planCode from your Paystack Dashboard. Paystack handles the recurring logic and emails.

{ name: "pro", planCode: "PLN_xxx" }

Local

Use amount and interval. The plugin stores the status locally, allowing you to manage custom recurring logic or one-off "access periods".

{ name: "starter", amount: 50000, interval: "monthly" }

2. One-Time Payments

Fixed Products

Define pre-configured products in your server settings and purchase them by name.

await authClient.paystack.transaction.initialize({
  product: "credits_50",
});

Ad-hoc Amounts

Charge dynamic amounts for top-ups, tips, or custom invoices.

await authClient.paystack.transaction.initialize({
  amount: 100000, // 1000 NGN
  currency: "NGN",
  metadata: { type: "donation" },
});

Limits & Seat Management

The plugin automatically enforces limits based on the active subscription.

Member Seat Limits

Purchased seats are stored in the subscription.seats field. The plugin hooks into member.create and invitation.create to block additions once the limit is reached.

Resource Limits (e.g., Teams)

Define limits in your plan config, and they will be checked during resource creation:

plans: [{ name: "pro", limits: { teams: 5, seats: 10 } }];

The plugin natively checks the teams limit if using the Better Auth Organization plugin.


Currency Support

The plugin supports the following currencies with automatic minimum transaction amount validation:

| Currency | Name | Minimum Amount | | -------- | ---------------------- | -------------- | | NGN | Nigerian Naira | ₦50.00 | | GHS | Ghanaian Cedi | ₵0.10 | | ZAR | South African Rand | R1.00 | | KES | Kenyan Shilling | KSh 3.00 | | USD | United States Dollar | $2.00 | | XOF | West African CFA Franc | CFA 100 |

Transactions below these thresholds will be rejected with a BAD_REQUEST error.

Advanced Usage

Organization Billing

Enable organization.enabled to bill organizations instead of users.

  • Auto Customer: Organizations get their own paystackCustomerCode.
  • Authorization: Use authorizeReference to control who can manage billing (e.g., Owners/Admins).

Inline Popup Modal

Use @alexasomba/paystack-browser for a seamless UI.

const { data } = await authClient.subscription.upgrade({ plan: "pro" });
if (data?.accessCode) {
  const paystack = createPaystack({ publicKey: "pk_test_..." });
  paystack.checkout({
    accessCode: data.accessCode,
    onSuccess: (res) =>
      authClient.paystack.transaction.verify({ reference: res.reference }),
  });
}

Scheduled Changes & Cancellation

Defer changes to the end of the current billing cycle:

  • Upgrades: Pass scheduleAtPeriodEnd: true in initializeTransaction().
  • Cancellations: Use authClient.subscription.cancel({ atPeriodEnd: true }) to keep the subscription active until the period ends.

Mid-Cycle Proration (prorateAndCharge)

The plugin can dynamically calculate the cost difference for immediate mid-cycle upgrades (like adding more seats). If the user has a saved Paystack authorization code, the plugin will execute a prorated charge for the remaining cycle days and immediately sync the new amount/seats.

await authClient.paystack.transaction.initialize({
  plan: "pro",
  quantity: 5, // Upgrading seats
  prorateAndCharge: true, // Will calculate and charge the prorated amount instantly
});

Trial Abuse Prevention

The plugin checks the referenceId history. If a trial was ever used (active, expired, or trialing), it will not be granted again, preventing resubscribe-abuse.

Lifecycle Hooks

React to billing events on the server by providing callbacks in your configuration:

Subscription Hooks (subscription.*)

  • onSubscriptionComplete: Called after successful transaction verification (Native or Local).
  • onSubscriptionCreated: Called when a subscription record is first initialized in the DB.
  • onSubscriptionUpdate: Called whenever a subscription's status or period is updated.
  • onSubscriptionCancel: Called when a user or organization cancels their subscription.
  • onSubscriptionDelete: Called when a subscription record is deleted.

Customer Hooks (top-level or organization.*)

  • onCustomerCreate: Called after the plugin successfully creates a Paystack customer.
  • getCustomerCreateParams: Return a custom object to override/extend the data sent to Paystack during customer creation.

Trial Hooks (subscription.plans[].freeTrial.*)

  • onTrialStart: Called when a new trial period begins.
  • onTrialEnd: Called when a trial period ends naturally or via manual upgrade.

Global Hook

  • onEvent: Receives every webhook event payload sent from Paystack for custom processing.

Authorization & Security

authorizeReference

Control who can manage billing for specific references (Users or Organizations).

paystack({
  subscription: {
    authorizeReference: async ({ user, referenceId, action }) => {
      // Example: Only allow Org Admins to initialize transactions
      if (referenceId.startsWith("org_")) {
        const member = await db.findOne({
          model: "member",
          where: [
            { field: "organizationId", value: referenceId },
            { field: "userId", value: user.id },
          ],
        });
        return member?.role === "admin";
      }
      return user.id === referenceId;
    },
  },
});

Client SDK Reference

The client plugin exposes fully typed methods under authClient.paystack and authClient.subscription.

authClient.subscription.upgrade / create

Initializes a transaction to create or upgrade a subscription.

type upgradeSubscription = {
  /**
   * The name of the plan to subscribe to.
   */
  plan: string;
  /**
   * The email of the subscriber. Defaults to the current user's email.
   */
  email?: string;
  /**
   * Amount to charge (if not using a Paystack Plan Code).
   */
  amount?: number;
  /**
   * Currency code (e.g., "NGN").
   */
  currency?: string;
  /**
   * The callback URL to redirect to after payment.
   */
  callbackURL?: string;
  /**
   * Additional metadata to store with the transaction.
   */
  metadata?: Record<string, any>;
  /**
   * Reference ID for the subscription owner (User ID or Org ID).
   * Defaults to the current user's ID.
   */
  referenceId?: string;
  /**
   * Number of seats to purchase (for team plans).
   */
  quantity?: number;
};

authClient.paystack.transaction.initialize

Same as upgrade, but can also be used for one-time payments by omitting plan and providing amount or product.

type initializeTransaction = {
  /**
   * Plan name (for subscriptions).
   */
  plan?: string;
  /**
   * Product name (for one-time purchases).
   */
  product?: string;
  /**
   * Amount to charge (if sending raw amount).
   */
  amount?: number;
  // ... same as upgradeSubscription
};

authClient.subscription.list

List subscriptions for a user or organization.

type listSubscriptions = {
  query?: {
    /**
     * Filter by reference ID (User ID or Org ID).
     */
    referenceId?: string;
  };
};

authClient.subscription.cancel / restore

Cancel or restore a subscription.

  • Cancel: Sets cancelAtPeriodEnd: true. The subscription remains active until the end of the current billing period, after which it moves to canceled.
  • Restore: Reactivates a subscription that is scheduled to cancel.
type cancelSubscription = {
  /**
   * The Paystack subscription code (e.g. SUB_...)
   */
  subscriptionCode: string;
  /**
   * The email token required by Paystack to manage the subscription.
   * Optional: The server will try to fetch it if omitted.
   */
  emailToken?: string;
};

Schema Reference

The plugin extends your database with the following fields and tables.

user

| Field | Type | Required | Description | | :--------------------- | :------- | :------- | :-------------------------------------------- | | paystackCustomerCode | string | No | The unique customer identifier from Paystack. |

organization

| Field | Type | Required | Description | | :--------------------- | :------- | :------- | :----------------------------------------------------------------------------------------- | | paystackCustomerCode | string | No | The unique customer identifier for the organization. | | email | string | No | The billing email for the organization. fallsback to organization owner's email if absent. |

subscription

| Field | Type | Required | Description | | :----------------------------- | :-------- | :------- | :------------------------------------------------------------------- | | plan | string | Yes | Lowercased name of the active plan. | | referenceId | string | Yes | Associated User ID or Organization ID. | | paystackCustomerCode | string | No | The Paystack customer code for this subscription. | | paystackSubscriptionCode | string | No | The unique code for the subscription (e.g., SUB_... or LOC_...). | | paystackTransactionReference | string | No | The reference of the transaction that started the subscription. | | paystackAuthorizationCode | string | No | Stored card authorization code for recurring charges (local plans). | | status | string | Yes | active, trialing, canceled, incomplete. | | periodStart | Date | No | Start date of the current billing period. | | periodEnd | Date | No | End date of the current billing period. | | trialStart | Date | No | Start date of the trial period. | | trialEnd | Date | No | End date of the trial period. | | cancelAtPeriodEnd | boolean | No | Whether to cancel at the end of the current period. | | seats | number | No | Purchased seat count for team billing. |

paystackTransaction

| Field | Type | Required | Description | | :------------ | :------- | :------- | :------------------------------------------------ | | reference | string | Yes | Unique transaction reference. | | referenceId | string | Yes | Associated User ID or Organization ID. | | userId | string | Yes | The ID of the user who initiated the transaction. | | amount | number | Yes | Transaction amount in smallest currency unit. | | currency | string | Yes | Currency code (e.g., "NGN"). | | status | string | Yes | success, pending, failed, abandoned. | | plan | string | No | Name of the plan associated with the transaction. | | product | string | No | Name of the product associated with the transaction. | | metadata | string | No | JSON string of extra transaction metadata. | | paystackId | string | No | The internal Paystack ID for the transaction. | | createdAt | Date | Yes | Transaction creation timestamp. | | updatedAt | Date | Yes | Transaction last update timestamp. |

paystackProduct

| Field | Type | Required | Description | | :------------ | :-------- | :------- | :------------------------------------------------ | | name | string | Yes | Product name. | | description | string | No | Product description. | | price | number | Yes | Price in smallest currency unit. | | currency | string | Yes | Currency code (e.g., "NGN"). | | quantity | number | No | Available stock quantity. | | unlimited | boolean | No | Whether the product has unlimited stock. | | paystackId | string | No | The internal Paystack Product ID. | | slug | string | Yes | Unique slug for the product. | | metadata | string | No | JSON string of extra product metadata. | | createdAt | Date | Yes | Product creation timestamp. | | updatedAt | Date | Yes | Product last update timestamp. |


Troubleshooting

  • Webhook Signature: Ensure PAYSTACK_WEBHOOK_SECRET is correct matches your Paystack Dashboard's secret key.
  • Email Verification: Use requireEmailVerification: true to prevent unverified checkouts.
  • Redirect Failures: Check your browser console; Paystack often returns 429 errors if you're hitting the test API too frequently.
  • Reference mismatches: Ensure referenceId is passed correctly for Organization billing.
  • Authorization Denied: Verify your authorizeReference logic is correctly checking user roles or organization memberships. Unauthorized attempts to verify transactions now return a 401 Unauthorized response to prevent data leaks.

Database Indexing

The plugin's schema definition includes recommended indexes and uniqueness constraints for performance. When you run npx better-auth migrate, these will be automatically applied to your database.

The following fields are indexed:

  • paystackTransaction: reference (unique), userId, referenceId.
  • subscription: paystackSubscriptionCode (unique), referenceId, paystackTransactionReference, paystackCustomerCode, plan.
  • user & organization: paystackCustomerCode.
  • paystackProduct: slug (unique), paystackId (unique).

Syncing Products

The plugin provides two ways to keep your product inventory in sync with Paystack:

1. Automated Inventory Sync (New)

Whenever a successful one-time payment is made (via webhook or manual verification), the plugin automatically calls syncProductQuantityFromPaystack. This fetches the real-time remaining quantity from the Paystack API and updates your local database record, ensuring your inventory is always accurate.

2. Manual Bulk Sync

You can synchronize all products with your local database using the /paystack/sync-products endpoint.

POST /api/auth/paystack/sync-products

🏗️ Development & Contributing

This repository is set up as a pnpm workspace. You can run and build examples via --filter.

# Install everything
pnpm install

# Build the core library
pnpm --filter "@alexasomba/better-auth-paystack" build

# Run Next.js example (Next.js + Better Auth)
pnpm --filter nextjs-better-auth-paystack dev

# Run TanStack Start example (TanStack Start + Better Auth)
pnpm --filter tanstack-start-better-auth-paystack dev

Contributions are welcome! Please open an issue or pull request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Roadmap

Future features planned for upcoming versions:

v1.1.0 - Manual Recurring Subscriptions (Available Now)

  • [x] Stored Authorization Codes: Securely store Paystack authorization codes from verified transactions.
  • [x] Charge Authorization Endpoint: Server-side endpoint (/charge-recurring) to charge stored cards for renewals.
  • [ ] Card Management UI: Let users view/delete saved payment methods (masked card data only) - Upcoming
  • [ ] Renewal Scheduler Integration: Documentation for integrating with Cloudflare Workers Cron, Vercel Cron, etc. - Upcoming

Note: For local-managed subscriptions (no planCode), the plugin now automatically captures and stores the authorization_code. You can trigger renewals using authClient.paystack.chargeRecurringSubscription({ subscriptionId }).

Future Considerations

  • [ ] Multi-currency support improvements
  • [ ] Invoice generation
  • [ ] Payment retry logic for failed renewals

Links