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

<div align="center">

![npm downloads](https://img.shields.io/npm/dm/@alexasomba/better-auth-paystack.svg)
[![GitHub stars](https://img.shields.io/github/stars/alexasomba/better-auth-paystack.svg?style=social&label=Star)](https://github.com/alexasomba/better-auth-paystack/stargazers)
[![GitHub release](https://img.shields.io/github/v/release/alexasomba/better-auth-paystack)](https://github.com/alexasomba/better-auth-paystack/releases)
[![bundlephobia](https://img.shields.io/bundlephobia/minzip/@alexasomba/better-auth-paystack)](https://bundlephobia.com/result?p=@alexasomba/better-auth-paystack)
[![Follow on Twitter](https://img.shields.io/twitter/follow/alexasomba?style=social)](https://twitter.com/alexasomba)
![GitHub License](https://img.shields.io/github/license/alexasomba/better-auth-paystack)

</div>

[**Live Demo (Tanstack Start)**](https://better-auth-paystack.gittech.workers.dev) | [**Source Code**](https://github.com/alexasomba/better-auth-paystack/tree/main/examples/tanstack)

## AI Agent Skills

This package publishes agent skills so AI coding agents can load package-specific guidance for setup, subscriptions, organization billing, TanStack Start integration, client APIs, webhooks, local subscription lifecycle, schema changes, and testing.

Use [TanStack Intent](https://www.npmjs.com/package/@tanstack/intent) when you want to explicitly list or load individual skills:

```bash
npx @tanstack/intent@latest list
npx @tanstack/intent@latest load @alexasomba/better-auth-paystack#better-auth-paystack-setup
npx @tanstack/intent@latest load @alexasomba/better-auth-paystack#paystack-testing-fixtures
```

If you use an AI agent, run `npx @tanstack/intent@latest install` in your project so the agent knows how to discover package skills.

This package also ships skills in the npm package under `skills/*/SKILL.md`, so projects can use [skills-npm](https://www.npmjs.com/package/skills-npm) to symlink installed package skills for compatible agents:

```bash
npm install @alexasomba/better-auth-paystack
npx skills-npm --yes
```

For this repository, maintainers can run `pnpm run skills:dry-run` to preview discovery through the TanStack example fixture or `pnpm run skills:install` to create local agent skill symlinks from that fixture.

## 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] **Subscription Channel Controls**: Restrict subscription checkout to specific Paystack payment channels such as card-only.
- [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 upgrades for local plans, using saved-card charges when possible and checkout fallback when interactive payment is required.
- [x] **Popup Modal Flow**: Optional support for Paystack's inline checkout experience via `@alexasomba/paystack-inline`.
- [x] **Webhook Security**: Pre-configured signature verification (HMAC-SHA512) and optional IP whitelisting.
- [x] **Transaction History**: Built-in support for listing and viewing local transaction records.

---

## Quick Start

### Prerequisites

- **Node.js**: `v22.0.0` or higher.
- **Better Auth**: `v1.6.9` or higher.

### 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-inline
```

### 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";
import { admin } from "better-auth/plugins";

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

export const auth = betterAuth({
  plugins: [
    admin(),
    paystack({
      paystackClient,
      webhook: { secret: process.env.PAYSTACK_WEBHOOK_SECRET! },
      createCustomerOnSignUp: true,
      subscription: {
        enabled: true,
        allowedPaymentChannels: ["card"], // Optional: enforce card-only subscriptions
        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" }],
      },
    }),
  ],
});
```

`webhook.secret` is the preferred webhook-signing config.
If you still have older code using top-level `paystackWebhookSecret`, it is treated as a deprecated alias and falls back to the same signature check.

### 4. Configure Client Plugin

```ts title="client.ts"
import { createAuthClient } from "better-auth/client";
import { paystackClient } from "@alexasomba/better-auth-paystack/client";
import { adminClient } from "better-auth/client/plugins";

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

### 5. Migrate Database Schema

```bash
npx better-auth migrate
```

---

## Migration Guide

Version `2.0.0` contains a security-focused breaking change.

- Removed public/client operator actions:
  - `authClient.paystack.syncProducts()`
  - `authClient.paystack.syncPlans()`
  - `authClient.paystack.chargeRecurringSubscription(...)`
- Removed public Better Auth endpoints for:
  - `/paystack/sync-products`
  - `/paystack/sync-plans`
  - `/paystack/charge-recurring`
- Added trusted server operations:
  - `chargeSubscriptionRenewal`
  - `syncPaystackProducts`
  - `syncPaystackPlans`

### Old

```ts
await authClient.paystack.syncProducts();
await authClient.paystack.syncPlans();
await authClient.paystack.chargeRecurringSubscription({
  subscriptionId: "sub_123",
});
```

### New

```ts
import {
  chargeSubscriptionRenewal,
  syncPaystackPlans,
  syncPaystackProducts,
  type ChargeRecurringSubscriptionResult,
  type PaystackSyncResult,
} from "@alexasomba/better-auth-paystack";

const ctx = { context: await auth.$context } as any;
const paystackOptions = {
  secretKey: process.env.PAYSTACK_SECRET_KEY!,
  webhook: { secret: process.env.PAYSTACK_WEBHOOK_SECRET! },
  paystackClient,
};

await syncPaystackProducts(ctx, paystackOptions);
await syncPaystackPlans(ctx, paystackOptions);
await chargeSubscriptionRenewal(ctx, paystackOptions, {
  subscriptionId: "sub_123",
});
```

These operations are intentionally server-only. Do not expose them through browser-triggered auth client calls.

---

## 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**: Organization owners and admins can manage billing by default. Use `organization.billingRoles` to extend the trusted role list, or `subscription.authorizeReference` when you need fully custom authorization.

```ts
paystack({
  subscription: {
    enabled: true,
    plans: [],
  },
  organization: {
    enabled: true,
    billingRoles: ["owner", "admin", "billing"],
  },
});
```

### Inline Popup Modal

Use `@alexasomba/paystack-inline` for a seamless UI.

```ts
const { data } = await authClient.subscription.upgrade({ plan: "pro" });
if (data?.kind === "checkout") {
  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({ subscriptionCode, 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).
For locally managed plans:

- If the subscription already has a reusable Paystack authorization code, the plugin charges the prorated delta off-session, records a local `paystackTransaction`, and immediately updates the subscription.
- If there is no reusable authorization code available (for example, transfer-based payments), the plugin initializes a new checkout for the prorated delta instead of silently upgrading without payment.
- If the prorated amount is below Paystack's minimum charge for the currency, the request is rejected so you can schedule the change for period end instead of undercharging.

```ts
const { data } = await authClient.paystack.transaction.initialize({
  plan: "pro",
  quantity: 5, // Upgrading seats
  prorateAndCharge: true, // Charges saved authorization or returns a checkout redirect for the delta
});

if (data?.kind === "checkout") {
  window.location.href = data.url;
}

if (data?.kind === "prorated") {
  console.log(data.message);
}
```

When the flow falls back to checkout, verify the returned transaction reference after payment. The plugin uses the stored proration metadata to apply the pending plan/seat change only after successful verification.

### Restricting Subscription Payment Channels

Use `subscription.allowedPaymentChannels` to constrain which Paystack checkout channels can be used for subscription flows.
This applies to standard subscription checkout, trial authorization flows, and interactive proration checkout fallbacks.

```ts
paystack({
  subscription: {
    enabled: true,
    allowedPaymentChannels: ["card"],
    plans: [{ name: "starter", amount: 50000, currency: "NGN", interval: "monthly" }],
  },
});
```

If a subscription payment is later verified with a disallowed channel, the plugin rejects activation instead of silently creating the subscription.

### Webhook Security

The plugin automatically verifies the `x-paystack-signature` header to ensure events are authentic. For an extra layer of security, you can enable **IP Whitelisting** to restrict processing to Paystack's official servers.

```ts
paystack({
  webhook: {
    secret: process.env.PAYSTACK_WEBHOOK_SECRET!,
    verifyIP: true, // Enable IP whitelisting (defaults to false for flexible proxy support)
    trustedIPs: ["52.31.139.75", "52.49.173.169", "52.214.14.220"], // Optional: override trusted IPs
  },
});
```

Resolution order for webhook signature verification is:
`webhook.secret` -> `paystackWebhookSecret` (deprecated) -> `secretKey`.

### 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.
- `onSubscriptionCancel`: Called when a user or organization cancels their subscription.

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

#### Global Hook

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

### Trusted Server Operations

Recurring renewals and Paystack catalog sync are intentionally not exposed through the browser auth client.
Invoke them from trusted backend code only:

```ts
import {
  chargeSubscriptionRenewal,
  reconcilePaystackTransaction,
  syncPaystackPlans,
  syncPaystackProducts,
} from "@alexasomba/better-auth-paystack";

const ctx = { context: await auth.$context } as any;

await chargeSubscriptionRenewal(ctx, paystackOptions, {
  subscriptionId: "sub_123",
});

const settlement = await reconcilePaystackTransaction(ctx, paystackOptions, {
  reference: "PAYSTACK_REFERENCE",
  source: "queue",
  referenceId: "user_or_org_id",
});

if (settlement.ok) {
  console.log(settlement.transaction.status, settlement.subscription.updated);
}

await syncPaystackProducts(ctx, paystackOptions);
await syncPaystackPlans(ctx, paystackOptions);
```

Use `reconcilePaystackTransaction` from webhook handlers, queue retries, cron jobs, or admin actions when trusted server code needs the same verification and local transaction/subscription side effects as the browser verify endpoint.

### 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 canonical methods under `authClient.paystack`, `authClient.transaction`, and `authClient.subscription`.

- `authClient.transaction.initialize`, `verify`, `list`
- `authClient.subscription.create`, `upgrade`, `cancel`, `restore`, `list`, `billingPortal`
- `authClient.paystack.config`, `listProducts`, `listPlans`, plus the transaction/subscription helpers above

Legacy compatibility aliases remain available for migration, but new code should use the canonical methods:

- `authClient.subscription.disable(...)` -> use `authClient.subscription.cancel(...)`
- `authClient.subscription.enable(...)` -> use `authClient.subscription.restore(...)`

### `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<string, unknown>;
  /**
   * 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;
  /**
   * For existing locally managed subscriptions, calculate a mid-cycle delta and either
   * charge the saved authorization or return a checkout redirect for interactive payment.
   */
  prorateAndCharge?: boolean;
  // ... same as upgradeSubscription
};

type initializeTransactionResult =
  | {
      kind: "checkout";
      url: string;
      reference: string;
      accessCode: string;
      redirect: true;
    }
  | { kind: "scheduled"; status: "success"; message: string; scheduled: true }
  | { kind: "prorated"; status: "success"; message: string; prorated: true };
```

### `authClient.subscription.list`

List subscriptions for a user or organization. Organization-scoped billing actions require an owner/admin membership by default. To allow roles such as `billing`, configure `organization.billingRoles`. For custom resources or deeper policy checks, configure `subscription.authorizeReference`.

```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 = {
  /**
   * Optional reference owner (user ID or org ID) when managing another billing entity.
   */
  referenceId?: string;
  /**
   * 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;
  /**
   * When true, keep the subscription active until the current period ends.
   */
  atPeriodEnd?: boolean;
};
```

## 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).

Proration upgrades and trusted renewal charges also persist `paystackTransaction` rows, so local transaction history stays aligned with successful off-session charges.

### Syncing Products

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

#### 1. Automated Inventory Sync

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. Trusted Manual Bulk Sync

The public `/paystack/sync-products` endpoint was removed in `2.0.0`.
Run the trusted server operation from backend code instead:

```ts
import { syncPaystackProducts } from "@alexasomba/better-auth-paystack";

const ctx = { context: await auth.$context } as any;

await syncPaystackProducts(ctx, paystackOptions);
```

### SDK Compatibility Note

The plugin now targets the official `@alexasomba/paystack-node` grouped client surface directly.
If you inject a custom client, it should match the real SDK methods used by the plugin such as `transaction.initialize`, `transaction.verify`, `transaction.chargeAuthorization`, `subscription.create`, `subscription.disable`, and `subscription.enable`.

---

## 🏗️ Development & Contributing

This repository is powered by **Vite+**. You use the `vp` CLI to manage the entire workspace.

```bash
# Install dependencies
vp i

# Check project health (format, lint, types)
vp check --fix

# Build the core library
vp build

# Run tests
vp test

# Run the TanStack Start example
vp run examples/tanstack 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] **Trusted Renewal Operation**: Server-side helper 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 automatically captures and stores the `authorization_code`. Trigger renewals from trusted backend code with `chargeSubscriptionRenewal(...)`.

### 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 Inline SDK: [alexasomba/paystack-inline](https://github.com/alexasomba/paystack-inline)
- [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)
