Skip to content

Better Auth plugin that integrates Paystack for customer creation, checkout, and Paystack-native subscription flows.

Notifications You must be signed in to change notification settings

alexasomba/better-auth-paystack

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Better Auth Paystack Plugin

Better Auth plugin that integrates Paystack for customer creation, checkout, and Paystack-native subscription flows.

Features

  • Optional Paystack customer creation on sign up (createCustomerOnSignUp)
  • Paystack checkout via transaction initialize + verify (redirect-first)
  • Paystack webhook signature verification (x-paystack-signature, HMAC-SHA512)
  • Local subscription records stored in your Better Auth database
  • Subscription management endpoints using Paystack’s email-token flows (/subscription/enable + /subscription/disable)
  • Reference ID support (user by default; org/team via referenceId + authorizeReference)

Installation

Install packages

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

Install from GitHub Packages (optional)

If you want to install this package from GitHub Packages (npm.pkg.github.com) instead of npmjs, configure a project-level .npmrc (or your user ~/.npmrc) to route the @alexasomba scope:

@alexasomba:registry=https://npm.pkg.github.com

Then authenticate and install:

# npm v9+ may require legacy auth prompts for private registries
npm login --scope=@alexasomba --auth-type=legacy --registry=https://npm.pkg.github.com

npm install @alexasomba/better-auth-paystack

Development (pnpm workspace)

This repo is set up as a pnpm workspace so you can install once at the repo root and run/build any example via --filter.

pnpm install

Build the library:

pnpm --filter "@alexasomba/better-auth-paystack" build

Run an example:

# Cloudflare Workers + Hono
pnpm --filter hono dev

# Next.js (OpenNext / Cloudflare)
pnpm --filter my-next-app dev

# TanStack Start
pnpm --filter tanstack-start dev

Build all workspace packages (library + examples):

pnpm -r build

If you want strict typing and the recommended server SDK client:

npm install @alexasomba/paystack-node

If your app has separate client + server bundles, install the plugin in both.

Configure the 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,
      // Paystack signs webhooks with an HMAC SHA-512 using your Paystack secret key.
      // Use the same secret key you configured in `createPaystack({ secretKey })`.
      paystackWebhookSecret: process.env.PAYSTACK_SECRET_KEY!,
      createCustomerOnSignUp: true,
      subscription: {
        enabled: true,
        plans: [
          {
            name: "starter",
            amount: 500000,
            currency: "NGN",
            // If you use Paystack Plans, prefer planCode + (optional) invoiceLimit.
            // planCode: "PLN_...",
            // invoiceLimit: 12,
          },
        ],
        authorizeReference: async ({ user, referenceId, action }, ctx) => {
          // Allow only the current user by default; authorize org/team IDs here.
          // return await canUserManageOrg(user.id, referenceId)
          return referenceId === user.id;
        },
      },
    }),
  ],
});

Configure the client plugin

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

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

Migrate / generate schema

The plugin adds fields/tables to your Better Auth database. Run the Better Auth CLI migration/generate step you already use in your project.

Webhooks

Endpoint URL

The plugin exposes a webhook endpoint at:

{AUTH_BASE}/paystack/webhook

Where {AUTH_BASE} is your Better Auth server base path (commonly /api/auth).

Signature verification

Paystack sends x-paystack-signature which is an HMAC-SHA512 of the raw payload signed with your secret key. The plugin verifies this using paystackWebhookSecret.

Recommended events

At minimum, enable the events your app depends on. For subscription flows, Paystack documents these as relevant:

  • charge.success
  • subscription.create
  • subscription.disable
  • subscription.not_renew

The plugin forwards all webhook payloads to onEvent (if provided) after signature verification.

Usage

Defining plans

Plans are referenced by their name (stored lowercased). For Paystack-native subscriptions you can either:

  • Use planCode (Paystack plan code). When planCode is provided, Paystack invalidates amount during transaction initialization.
  • Or use amount (smallest currency unit) for simple payments.

Frontend checkout (redirect)

This flow matches Paystack’s transaction initialize/verify APIs:

  1. Call POST {AUTH_BASE}/paystack/transaction/initialize
  2. Redirect the user to the returned Paystack url
  3. On your callback route/page, call POST {AUTH_BASE}/paystack/transaction/verify (this updates local subscription state)

Example (typed via Better Auth client plugin):

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

const plugins = [paystackClient({ subscription: true })];

const authClient = createAuthClient({
  // Your Better Auth base URL (commonly "/api/auth" in Next.js)
  baseURL: "/api/auth",
  plugins,
});

// Start checkout
const init = await authClient.paystack.transaction.initialize(
  {
    plan: "starter",
    callbackURL: `${window.location.origin}/billing/paystack/callback`,
    // Optional for org/team billing (requires authorizeReference)
    // referenceId: "org_123",
  },
  { throw: true },
);
// { url, reference, accessCode, redirect: true }
if (init?.url) window.location.href = init.url;

// On your callback page/route
const reference = new URLSearchParams(window.location.search).get("reference");
if (reference) {
  await authClient.paystack.transaction.verify({ reference }, { throw: true });
}

Server-side (no HTTP fetch needed):

// On the server you can call the endpoints directly:
// const init = await auth.api.initializeTransaction({ headers: req.headers, body: { plan: "starter" } })
// const verify = await auth.api.verifyTransaction({ headers: req.headers, body: { reference } })

Inline modal checkout (optional)

If you prefer an inline checkout experience, initialize the transaction the same way and use @alexasomba/paystack-browser in your UI. This plugin does not render UI — it only provides server endpoints.

Listing local subscriptions

List subscription rows stored by this plugin:

GET {AUTH_BASE}/paystack/subscription/list-local

You can optionally pass referenceId as a query param (requires authorizeReference when it’s not the current user):

GET {AUTH_BASE}/paystack/subscription/list-local?referenceId=org_123

Enabling / disabling a subscription

Paystack requires both the subscription code and the email token.

For convenience, the plugin lets you omit emailToken and will attempt to fetch it from Paystack using the subscription code (via Subscription fetch, with a fallback to Manage Link).

  • POST {AUTH_BASE}/paystack/subscription/enable with { subscriptionCode, emailToken? }
  • POST {AUTH_BASE}/paystack/subscription/disable with { subscriptionCode, emailToken? }

Paystack documents these as code + token. If the server cannot fetch emailToken, you can still provide it explicitly (e.g., from the Subscription API or your Paystack dashboard).

Schema

The plugin adds the following to your Better Auth database schema.

user

Field Type Required Default
paystackCustomerCode string no

subscription (only when subscription.enabled: true)

Field Type Required Default
plan string yes
referenceId string yes
paystackCustomerCode string no
paystackSubscriptionCode string no
paystackTransactionReference string no
status string no "incomplete"
periodStart date no
periodEnd date no
trialStart date no
trialEnd date no
cancelAtPeriodEnd boolean no false
groupId string no
seats number no

Options

Main options:

  • paystackClient (recommended: createPaystack({ secretKey }))
  • paystackWebhookSecret
  • createCustomerOnSignUp?
  • onCustomerCreate?, getCustomerCreateParams?
  • onEvent?
  • schema? (override/mapping)

Subscription options (when subscription.enabled: true):

  • plans (array or async function)
  • requireEmailVerification?
  • authorizeReference?
  • onSubscriptionComplete?, onSubscriptionUpdate?, onSubscriptionDelete?

Troubleshooting

  • Webhook signature mismatch: ensure your server receives the raw body, and PAYSTACK_WEBHOOK_SECRET matches the secret key used by Paystack to sign events.
  • Subscription list returns empty: verify you’re passing the correct referenceId, and that authorizeReference allows it.
  • Transaction initializes but verify doesn’t update: ensure you call the verify endpoint after redirect, and confirm Paystack returns status: "success" for the reference.

Links

About

Better Auth plugin that integrates Paystack for customer creation, checkout, and Paystack-native subscription flows.

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors 2

  •  
  •