Better Auth plugin that integrates Paystack for customer creation, checkout, and Paystack-native subscription flows.
- 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)
npm install better-auth @alexasomba/better-auth-paystackIf 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.comThen 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-paystackThis 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 installBuild the library:
pnpm --filter "@alexasomba/better-auth-paystack" buildRun 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 devBuild all workspace packages (library + examples):
pnpm -r buildIf you want strict typing and the recommended server SDK client:
npm install @alexasomba/paystack-nodeIf your app has separate client + server bundles, install the plugin in both.
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;
},
},
}),
],
});import { createAuthClient } from "better-auth/client";
import { paystackClient } from "@alexasomba/better-auth-paystack/client";
export const client = createAuthClient({
plugins: [paystackClient({ subscription: true })],
});The plugin adds fields/tables to your Better Auth database. Run the Better Auth CLI migration/generate step you already use in your project.
The plugin exposes a webhook endpoint at:
{AUTH_BASE}/paystack/webhook
Where {AUTH_BASE} is your Better Auth server base path (commonly /api/auth).
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.
At minimum, enable the events your app depends on. For subscription flows, Paystack documents these as relevant:
charge.successsubscription.createsubscription.disablesubscription.not_renew
The plugin forwards all webhook payloads to onEvent (if provided) after signature verification.
Plans are referenced by their name (stored lowercased). For Paystack-native subscriptions you can either:
- Use
planCode(Paystack plan code). WhenplanCodeis provided, Paystack invalidatesamountduring transaction initialization. - Or use
amount(smallest currency unit) for simple payments.
This flow matches Paystack’s transaction initialize/verify APIs:
- Call
POST {AUTH_BASE}/paystack/transaction/initialize - Redirect the user to the returned Paystack
url - 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 } })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.
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
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/enablewith{ subscriptionCode, emailToken? }POST {AUTH_BASE}/paystack/subscription/disablewith{ 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).
The plugin adds the following to your Better Auth database schema.
| Field | Type | Required | Default |
|---|---|---|---|
paystackCustomerCode |
string |
no | — |
| 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 | — |
Main options:
paystackClient(recommended:createPaystack({ secretKey }))paystackWebhookSecretcreateCustomerOnSignUp?onCustomerCreate?,getCustomerCreateParams?onEvent?schema?(override/mapping)
Subscription options (when subscription.enabled: true):
plans(array or async function)requireEmailVerification?authorizeReference?onSubscriptionComplete?,onSubscriptionUpdate?,onSubscriptionDelete?
- Webhook signature mismatch: ensure your server receives the raw body, and
PAYSTACK_WEBHOOK_SECRETmatches the secret key used by Paystack to sign events. - Subscription list returns empty: verify you’re passing the correct
referenceId, and thatauthorizeReferenceallows 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.
- Paystack Webhooks: https://paystack.com/docs/payments/webhooks/
- Paystack Transaction API: https://paystack.com/docs/api/transaction/
- Paystack Subscription API: https://paystack.com/docs/api/subscription/
- Paystack Plan API: https://paystack.com/docs/api/plan/