https://github.com/alexasomba/better-auth-paystack
A TypeScript-first plugin that integrates Paystack into Better Auth, enabling seamless customer creation, secure checkout, webhook verification, and native subscription flows. Designed for modern frameworks like Tanstack Start, Next.js, Hono, and Cloudflare Workers, it provides typed APIs, subscription management, and end-to-end payment integration
https://github.com/alexasomba/better-auth-paystack
auth better-auth egp ghs kes ngn payments paystack paystack-api paystack-sdk saas subscription tanstack-start typescript usd xof zar
Last synced: 14 days ago
JSON representation
A TypeScript-first plugin that integrates Paystack into Better Auth, enabling seamless customer creation, secure checkout, webhook verification, and native subscription flows. Designed for modern frameworks like Tanstack Start, Next.js, Hono, and Cloudflare Workers, it provides typed APIs, subscription management, and end-to-end payment integration
- Host: GitHub
- URL: https://github.com/alexasomba/better-auth-paystack
- Owner: alexasomba
- License: mit
- Created: 2025-12-15T16:42:29.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-02-23T17:41:18.000Z (20 days ago)
- Last Synced: 2026-02-23T23:47:11.344Z (20 days ago)
- Topics: auth, better-auth, egp, ghs, kes, ngn, payments, paystack, paystack-api, paystack-sdk, saas, subscription, tanstack-start, typescript, usd, xof, zar
- Language: TypeScript
- Homepage: https://better-auth-paystack.gittech.workers.dev
- Size: 2.31 MB
- Stars: 10
- Watchers: 0
- Forks: 2
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Codeowners: .github/CODEOWNERS
Awesome Lists containing this project
README
# Better Auth Paystack Plugin
A TypeScript-first plugin that integrates Paystack into [Better Auth](https://www.better-auth.com), 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)**](https://better-auth-paystack.gittech.workers.dev) | [**Source Code**](https://github.com/alexasomba/better-auth-paystack/tree/main/examples/tanstack)
## 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
```bash
npm install better-auth @alexasomba/better-auth-paystack @alexasomba/paystack-node
```
#### Optional: Browser SDK (for Popup Modals)
```bash
npm install @alexasomba/paystack-browser
```
### 2. Configure Environment Variables
```env
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
```ts title="auth.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,
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
```ts title="client.ts"
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
```bash
npx better-auth migrate
```
---
## Billing Patterns
### 1. Subscriptions
#### Native (Recommended)
Use `planCode` from your Paystack Dashboard. Paystack handles the recurring logic and emails.
```ts
{ 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".
```ts
{ 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.
```ts
await authClient.paystack.transaction.initialize({
product: "credits_50",
});
```
#### Ad-hoc Amounts
Charge dynamic amounts for top-ups, tips, or custom invoices.
```ts
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:
```ts
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.
```ts
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.
```ts
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).
```ts
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.
```ts
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;
/**
* 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`.
```ts
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.
```ts
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.
```ts
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.
```bash
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`.
```bash
# 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](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
- GitHub Repository: [alexasomba/better-auth-paystack](https://github.com/alexasomba/better-auth-paystack)
- Comprehensive and up-to-date Paystack Node SDK: [alexasomba/paystack-node](https://github.com/alexasomba/paystack-node)
- Comprehensive and up-to-date Paystack Browser SDK: [alexasomba/paystack-browser](https://github.com/alexasomba/paystack-browser)
- [TanStack Start Example Implementation](https://github.com/alexasomba/better-auth-paystack/tree/main/examples/tanstack)
- 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/
- [Better Auth Documentation](https://www.better-auth.com/docs)