An open API service indexing awesome lists of open source software.

https://github.com/alexasomba/better-auth-paystack

Better Auth plugin that integrates Paystack for customer creation, checkout, and Paystack-native subscription flows.
https://github.com/alexasomba/better-auth-paystack

auth better-auth nextjs payments paystack paystack-api paystack-sdk subscription typescript

Last synced: 4 days ago
JSON representation

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

Awesome Lists containing this project

README

          

# 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

```bash
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:

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

Then authenticate and install:

```bash
# 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`.

```bash
pnpm install
```

Build the library:

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

Run an example:

```bash
# 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):

```bash
pnpm -r build
```

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

```bash
npm install @alexasomba/paystack-node
```

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

### Configure the server plugin

```ts
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

```ts
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):

```ts
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):

```ts
// 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

- 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/