@alexasomba/better-auth-paystack 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/client.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/version-DpVME9MV.mjs +6 -0
- package/dist/version-DpVME9MV.mjs.map +1 -0
- package/package.json +5 -2
- package/skills/billing-catalog-and-limits/SKILL.md +184 -0
- package/skills/organization-billing/SKILL.md +139 -0
- package/skills/setup/SKILL.md +144 -0
- package/skills/subscriptions-and-transactions/SKILL.md +145 -0
- package/skills/tanstack-start/SKILL.md +161 -0
- package/dist/version-C_50YiuM.mjs +0 -6
- package/dist/version-C_50YiuM.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -15,6 +15,17 @@ A TypeScript-first plugin that integrates Paystack into [Better Auth](https://ww
|
|
|
15
15
|
|
|
16
16
|
[**Live Demo (Tanstack Start)**](https://better-auth-paystack.gittech.workers.dev) | [**Source Code**](https://github.com/alexasomba/better-auth-paystack/tree/main/examples/tanstack)
|
|
17
17
|
|
|
18
|
+
## AI Agent Skills
|
|
19
|
+
|
|
20
|
+
This package publishes [TanStack Intent](https://www.npmjs.com/package/@tanstack/intent) skills so AI coding agents can load package-specific guidance for setup, subscriptions, organization billing, and TanStack Start integration.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @tanstack/intent@latest list
|
|
24
|
+
npx @tanstack/intent@latest load @alexasomba/better-auth-paystack#setup
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If you use an AI agent, run `npx @tanstack/intent@latest install` in your project so the agent knows how to discover package skills.
|
|
28
|
+
|
|
18
29
|
## Features
|
|
19
30
|
|
|
20
31
|
- [x] **Billing Patterns**: Support for Paystack-native plans, local-managed subscriptions, and one-time payments (products/amounts).
|
package/dist/client.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -329,7 +329,7 @@ declare const getConfig: <P extends string = "/get-config">(options: AnyPaystack
|
|
|
329
329
|
}>;
|
|
330
330
|
//#endregion
|
|
331
331
|
//#region src/version.d.ts
|
|
332
|
-
declare const PACKAGE_VERSION = "2.
|
|
332
|
+
declare const PACKAGE_VERSION = "2.4.1";
|
|
333
333
|
//#endregion
|
|
334
334
|
//#region src/operations.d.ts
|
|
335
335
|
declare function syncPaystackProducts(ctx: GenericEndpointContext, options: AnyPaystackOptions): Promise<PaystackSyncResult>;
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
1
|
+
import { t as PACKAGE_VERSION } from "./version-DpVME9MV.mjs";
|
|
2
2
|
import { HIDE_METADATA, defineErrorCodes, logger } from "better-auth";
|
|
3
3
|
import { defu } from "defu";
|
|
4
4
|
import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version-DpVME9MV.mjs","names":[],"sources":["../src/version.ts"],"sourcesContent":["export const PACKAGE_VERSION = \"2.4.1\"; // x-release-please-version\n"],"mappings":";AAAA,MAAa,kBAAkB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexasomba/better-auth-paystack",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "Production-ready Paystack billing plugin for Better Auth. Supports subscriptions, one-time payments, organization billing, secure webhooks and more",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"africa",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"saas",
|
|
25
25
|
"south-africa",
|
|
26
26
|
"subscriptions",
|
|
27
|
+
"tanstack-intent",
|
|
27
28
|
"usd",
|
|
28
29
|
"zar"
|
|
29
30
|
],
|
|
@@ -44,7 +45,8 @@
|
|
|
44
45
|
}
|
|
45
46
|
],
|
|
46
47
|
"files": [
|
|
47
|
-
"dist"
|
|
48
|
+
"dist",
|
|
49
|
+
"skills"
|
|
48
50
|
],
|
|
49
51
|
"type": "module",
|
|
50
52
|
"sideEffects": false,
|
|
@@ -77,6 +79,7 @@
|
|
|
77
79
|
"@commitlint/config-conventional": "^21.0.0",
|
|
78
80
|
"@eslint/compat": "^2.1.0",
|
|
79
81
|
"@eslint/js": "^10.0.1",
|
|
82
|
+
"@tanstack/intent": "^0.0.40",
|
|
80
83
|
"@types/node": "^24.12.3",
|
|
81
84
|
"@typescript-eslint/eslint-plugin": "^8.59.2",
|
|
82
85
|
"@typescript-eslint/parser": "^8.59.2",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: billing-catalog-and-limits
|
|
3
|
+
description: >
|
|
4
|
+
Configure products, Paystack-native plans, local-managed plans, free trials, seat billing, resource limits, and catalog sync in @alexasomba/better-auth-paystack. Use when tasks mention planCode, freeTrial, trial eligibility, seatAmount, seatPlanCode, limits, products, syncPaystackProducts, or syncPaystackPlans.
|
|
5
|
+
type: core
|
|
6
|
+
library: "@alexasomba/better-auth-paystack"
|
|
7
|
+
library_version: "2.4.1" # x-release-please-version
|
|
8
|
+
sources:
|
|
9
|
+
- "alexasomba/better-auth-paystack:README.md"
|
|
10
|
+
- "alexasomba/better-auth-paystack:src/types.ts"
|
|
11
|
+
- "alexasomba/better-auth-paystack:src/routes.ts"
|
|
12
|
+
- "alexasomba/better-auth-paystack:src/operations.ts"
|
|
13
|
+
- "alexasomba/better-auth-paystack:src/utils.ts"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
Configure catalog data on the server plugin. Products are one-time purchasable catalog items. Plans are subscription catalog items.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { paystack } from "@alexasomba/better-auth-paystack";
|
|
22
|
+
|
|
23
|
+
export const paystackPlugin = paystack({
|
|
24
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
25
|
+
webhook: {
|
|
26
|
+
secret: process.env.PAYSTACK_WEBHOOK_SECRET!,
|
|
27
|
+
},
|
|
28
|
+
products: {
|
|
29
|
+
products: [
|
|
30
|
+
{
|
|
31
|
+
name: "credits_50",
|
|
32
|
+
amount: 200_000,
|
|
33
|
+
currency: "NGN",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
subscription: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
plans: [
|
|
40
|
+
{
|
|
41
|
+
name: "pro",
|
|
42
|
+
amount: 500_000,
|
|
43
|
+
currency: "NGN",
|
|
44
|
+
interval: "monthly",
|
|
45
|
+
planCode: "PLN_pro_monthly",
|
|
46
|
+
paystackId: "1001",
|
|
47
|
+
freeTrial: {
|
|
48
|
+
days: 14,
|
|
49
|
+
},
|
|
50
|
+
limits: {
|
|
51
|
+
seats: 10,
|
|
52
|
+
teams: 5,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Core Patterns
|
|
61
|
+
|
|
62
|
+
### Choose Paystack-native plans for simple recurring billing
|
|
63
|
+
|
|
64
|
+
Use `planCode` from the Paystack Dashboard when Paystack should manage the recurring subscription.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
{
|
|
68
|
+
name: "pro",
|
|
69
|
+
amount: 500_000,
|
|
70
|
+
currency: "NGN",
|
|
71
|
+
interval: "monthly",
|
|
72
|
+
planCode: "PLN_pro_monthly",
|
|
73
|
+
paystackId: "1001",
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Paystack-native plans are the right default for fixed-price recurring billing. Do not use native plans for flows that require local seat proration or locally managed renewals.
|
|
78
|
+
|
|
79
|
+
### Omit planCode for local-managed subscriptions
|
|
80
|
+
|
|
81
|
+
Local-managed plans are tracked in your database and renewed from stored Paystack authorizations by trusted backend code.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
{
|
|
85
|
+
name: "local-team",
|
|
86
|
+
amount: 1_000_000,
|
|
87
|
+
currency: "NGN",
|
|
88
|
+
interval: "monthly",
|
|
89
|
+
seatAmount: 100_000,
|
|
90
|
+
limits: {
|
|
91
|
+
seats: 10,
|
|
92
|
+
teams: 3,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For local-managed subscriptions, no `planCode` means the plugin captures and stores the authorization code after transaction verification. Trigger renewals from server code with `chargeSubscriptionRenewal`.
|
|
98
|
+
|
|
99
|
+
### Configure trials on plans
|
|
100
|
+
|
|
101
|
+
Trials are declared per plan:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
{
|
|
105
|
+
name: "starter",
|
|
106
|
+
amount: 250_000,
|
|
107
|
+
currency: "NGN",
|
|
108
|
+
interval: "monthly",
|
|
109
|
+
planCode: "PLN_starter",
|
|
110
|
+
paystackId: "1002",
|
|
111
|
+
freeTrial: {
|
|
112
|
+
days: 7,
|
|
113
|
+
onTrialStart: async (subscription) => {
|
|
114
|
+
await notifyTrialStarted(subscription.referenceId);
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The plugin checks previous subscription history for the `referenceId`. If a trial was ever used, expired, or marked `trialing`, another trial is denied for that reference. Do not build UI that promises repeat trials for the same user or organization.
|
|
121
|
+
|
|
122
|
+
### Configure seats and resource limits
|
|
123
|
+
|
|
124
|
+
Use `limits` for app resource enforcement and `seatAmount` for local seat billing amounts:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
{
|
|
128
|
+
name: "team",
|
|
129
|
+
amount: 1_000_000,
|
|
130
|
+
currency: "NGN",
|
|
131
|
+
interval: "monthly",
|
|
132
|
+
seatAmount: 100_000,
|
|
133
|
+
seatPlanCode: "PLN_extra_seat",
|
|
134
|
+
limits: {
|
|
135
|
+
seats: 10,
|
|
136
|
+
teams: 3,
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`seatPriceId` is a deprecated alias. Use `seatAmount` in new code. `seatPlanCode` is only useful when a Paystack plan code exists for extra seats.
|
|
142
|
+
|
|
143
|
+
### Sync products and plans from trusted server jobs
|
|
144
|
+
|
|
145
|
+
Use server-only helpers to mirror Paystack catalog data into local tables:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { syncPaystackPlans, syncPaystackProducts } from "@alexasomba/better-auth-paystack";
|
|
149
|
+
|
|
150
|
+
export async function syncCatalog(ctx: unknown, options: unknown) {
|
|
151
|
+
await syncPaystackProducts(ctx, options);
|
|
152
|
+
await syncPaystackPlans(ctx, options);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
These operations are not browser client actions. Run them from cron, admin-only server functions, CI jobs, or deployment tasks.
|
|
157
|
+
|
|
158
|
+
## Common Mistakes
|
|
159
|
+
|
|
160
|
+
### Using native planCode for local seat/proration behavior
|
|
161
|
+
|
|
162
|
+
Wrong:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
{
|
|
166
|
+
name: "team",
|
|
167
|
+
planCode: "PLN_team",
|
|
168
|
+
seatAmount: 100_000,
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Correct: omit `planCode` when the plan needs local seat billing, local renewals, or prorated seat changes.
|
|
173
|
+
|
|
174
|
+
### Treating products like subscription plans
|
|
175
|
+
|
|
176
|
+
Products are one-time purchases. Plans are subscriptions. Use transaction initialization for product purchases and subscription actions for plans.
|
|
177
|
+
|
|
178
|
+
### Expecting product and plan tables to be optional
|
|
179
|
+
|
|
180
|
+
`paystackProduct` and `paystackPlan` schema tables are always included by the plugin. Do not remove them in compatibility-preserving releases.
|
|
181
|
+
|
|
182
|
+
### Trusting trial state without verification
|
|
183
|
+
|
|
184
|
+
Trial metadata is created during subscription checkout and finalized through transaction/webhook handling. Always verify the Paystack reference and rely on persisted subscription state before granting paid access.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: organization-billing
|
|
3
|
+
description: >
|
|
4
|
+
Configure organization billing in @alexasomba/better-auth-paystack. Use for organization.enabled, Better Auth organization plugin setup, owner/admin default billing authorization, subscription.authorizeReference, organization Paystack customers, seats, invitations, members, and team limits.
|
|
5
|
+
type: core
|
|
6
|
+
library: "@alexasomba/better-auth-paystack"
|
|
7
|
+
library_version: "2.4.1" # x-release-please-version
|
|
8
|
+
sources:
|
|
9
|
+
- "alexasomba/better-auth-paystack:README.md"
|
|
10
|
+
- "alexasomba/better-auth-paystack:src/index.ts"
|
|
11
|
+
- "alexasomba/better-auth-paystack:src/utils.ts"
|
|
12
|
+
- "alexasomba/better-auth-paystack:src/limits.ts"
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Install the Better Auth organization plugin and enable Paystack organization billing:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { betterAuth } from "better-auth";
|
|
21
|
+
import { organization } from "better-auth/plugins/organization";
|
|
22
|
+
import { paystack } from "@alexasomba/better-auth-paystack";
|
|
23
|
+
|
|
24
|
+
export const auth = betterAuth({
|
|
25
|
+
plugins: [
|
|
26
|
+
organization(),
|
|
27
|
+
paystack({
|
|
28
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
29
|
+
webhook: {
|
|
30
|
+
secret: process.env.PAYSTACK_WEBHOOK_SECRET!,
|
|
31
|
+
},
|
|
32
|
+
subscription: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
plans: [
|
|
35
|
+
{
|
|
36
|
+
name: "team",
|
|
37
|
+
amount: 1_000_000,
|
|
38
|
+
currency: "NGN",
|
|
39
|
+
interval: "monthly",
|
|
40
|
+
planCode: "PLN_team",
|
|
41
|
+
paystackId: "1002",
|
|
42
|
+
limits: {
|
|
43
|
+
seats: 10,
|
|
44
|
+
teams: 3,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
organization: {
|
|
50
|
+
enabled: true,
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Core Patterns
|
|
58
|
+
|
|
59
|
+
### Rely on the safe default for billing authorization
|
|
60
|
+
|
|
61
|
+
When `subscription.authorizeReference` is not supplied, organization billing actions require membership role `owner` or `admin`.
|
|
62
|
+
|
|
63
|
+
Ordinary members are rejected by default. Do not write UI or tests that assume any member can create, upgrade, cancel, or restore organization subscriptions.
|
|
64
|
+
|
|
65
|
+
### Override authorization explicitly for custom workflows
|
|
66
|
+
|
|
67
|
+
Use `subscription.authorizeReference` when a product intentionally allows non-owner/admin billing access:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
paystack({
|
|
71
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
72
|
+
subscription: {
|
|
73
|
+
enabled: true,
|
|
74
|
+
plans: [],
|
|
75
|
+
authorizeReference: async ({ user, referenceId }, ctx) => {
|
|
76
|
+
if (referenceId === user.id) return true;
|
|
77
|
+
|
|
78
|
+
const memberships = await ctx.context.adapter.findMany({
|
|
79
|
+
model: "member",
|
|
80
|
+
where: [
|
|
81
|
+
{ field: "userId", value: user.id },
|
|
82
|
+
{ field: "organizationId", value: referenceId },
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return memberships.some((membership) => {
|
|
87
|
+
const role = (membership as { role?: string }).role;
|
|
88
|
+
return role === "owner" || role === "admin" || role === "billing";
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
organization: {
|
|
93
|
+
enabled: true,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
When supplied, `authorizeReference` is authoritative. Include all user and organization cases you want to allow.
|
|
99
|
+
|
|
100
|
+
### Create organization Paystack customers
|
|
101
|
+
|
|
102
|
+
When organization billing is enabled and the organization plugin is present, the plugin wires organization creation hooks. It tries to create a Paystack customer for the organization and stores `paystackCustomerCode`.
|
|
103
|
+
|
|
104
|
+
Customize creation params when needed:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
organization: {
|
|
108
|
+
enabled: true,
|
|
109
|
+
getCustomerCreateParams: async (org) => ({
|
|
110
|
+
metadata: JSON.stringify({
|
|
111
|
+
organizationId: org.id,
|
|
112
|
+
billingSource: "better-auth-paystack",
|
|
113
|
+
}),
|
|
114
|
+
}),
|
|
115
|
+
onCustomerCreate: async ({ paystackCustomer, organization }, ctx) => {
|
|
116
|
+
await ctx.context.adapter.update({
|
|
117
|
+
model: "organization",
|
|
118
|
+
where: [{ field: "id", value: organization.id }],
|
|
119
|
+
update: {
|
|
120
|
+
paystackCustomerCode: paystackCustomer.customer_code,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Common Mistakes
|
|
128
|
+
|
|
129
|
+
### Enabling organization billing without the organization plugin
|
|
130
|
+
|
|
131
|
+
If `organization.enabled` is true but Better Auth's organization plugin is missing, Paystack logs a clear error and skips organization hook wiring. Add `organization()` before relying on organization customer creation, members, invitations, seats, or team hooks.
|
|
132
|
+
|
|
133
|
+
### Assuming member limits apply without subscription plans
|
|
134
|
+
|
|
135
|
+
Seat and team limits come from subscription plan limits. If a plan has no relevant `limits` value, the plugin cannot enforce that limit.
|
|
136
|
+
|
|
137
|
+
### Forgetting seat sync after membership changes
|
|
138
|
+
|
|
139
|
+
The plugin hooks member/invitation lifecycle events when the organization plugin is present. If you implement custom membership mutation routes outside Better Auth's adapter hooks, explicitly re-check or sync seat state in that custom path.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: setup
|
|
3
|
+
description: >
|
|
4
|
+
Configure @alexasomba/better-auth-paystack with Better Auth. Use when adding the paystack() server plugin, paystackClient() client plugin, schema overrides, products/plans, webhook secrets, or canonical authClient.paystack/subscription/transaction actions.
|
|
5
|
+
type: core
|
|
6
|
+
library: "@alexasomba/better-auth-paystack"
|
|
7
|
+
library_version: "2.4.1" # x-release-please-version
|
|
8
|
+
sources:
|
|
9
|
+
- "alexasomba/better-auth-paystack:README.md"
|
|
10
|
+
- "alexasomba/better-auth-paystack:src/index.ts"
|
|
11
|
+
- "alexasomba/better-auth-paystack:src/client.ts"
|
|
12
|
+
- "alexasomba/better-auth-paystack:src/schema.ts"
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Install the package alongside Better Auth and a Paystack client:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { betterAuth } from "better-auth";
|
|
21
|
+
import { createPaystack } from "@alexasomba/paystack-node";
|
|
22
|
+
import { paystack } from "@alexasomba/better-auth-paystack";
|
|
23
|
+
|
|
24
|
+
const paystackSdk = createPaystack({
|
|
25
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const auth = betterAuth({
|
|
29
|
+
database: {
|
|
30
|
+
provider: "sqlite",
|
|
31
|
+
url: process.env.DATABASE_URL!,
|
|
32
|
+
},
|
|
33
|
+
plugins: [
|
|
34
|
+
paystack({
|
|
35
|
+
paystackClient: paystackSdk,
|
|
36
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
37
|
+
webhook: {
|
|
38
|
+
secret: process.env.PAYSTACK_WEBHOOK_SECRET!,
|
|
39
|
+
},
|
|
40
|
+
subscription: {
|
|
41
|
+
enabled: true,
|
|
42
|
+
plans: [
|
|
43
|
+
{
|
|
44
|
+
name: "pro",
|
|
45
|
+
amount: 500_000,
|
|
46
|
+
currency: "NGN",
|
|
47
|
+
interval: "monthly",
|
|
48
|
+
planCode: "PLN_pro_monthly",
|
|
49
|
+
paystackId: "123456",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Add the client plugin in browser-safe code:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { createAuthClient } from "better-auth/client";
|
|
62
|
+
import { paystackClient } from "@alexasomba/better-auth-paystack/client";
|
|
63
|
+
|
|
64
|
+
export const authClient = createAuthClient({
|
|
65
|
+
plugins: [paystackClient()],
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Core Patterns
|
|
70
|
+
|
|
71
|
+
### Use canonical client namespaces
|
|
72
|
+
|
|
73
|
+
The client plugin exposes these namespaces:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
await authClient.paystack.getConfig();
|
|
77
|
+
await authClient.transaction.initialize({ amount: 500_000, email: "user@example.com" });
|
|
78
|
+
await authClient.transaction.verify({ reference: "trx_ref" });
|
|
79
|
+
await authClient.transaction.list();
|
|
80
|
+
await authClient.subscription.create({ plan: "pro" });
|
|
81
|
+
await authClient.subscription.upgrade({ plan: "team" });
|
|
82
|
+
await authClient.subscription.cancel({ subscriptionId: "sub_id" });
|
|
83
|
+
await authClient.subscription.restore({ subscriptionId: "sub_id" });
|
|
84
|
+
await authClient.subscription.list();
|
|
85
|
+
await authClient.subscription.billingPortal();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`subscription.disable` and `subscription.enable` still exist as deprecated aliases in the 2.x line. Prefer `cancel` and `restore` in new code.
|
|
89
|
+
|
|
90
|
+
### Keep schema behavior stable
|
|
91
|
+
|
|
92
|
+
The plugin always contributes Paystack product and plan tables:
|
|
93
|
+
|
|
94
|
+
- `paystackProduct`
|
|
95
|
+
- `paystackPlan`
|
|
96
|
+
|
|
97
|
+
Subscription tables are included when `subscription.enabled` is true. User and transaction tables are always included. Organization fields are included when `organization.enabled` is true.
|
|
98
|
+
|
|
99
|
+
Use Better Auth-style schema overrides only to rename models or fields. Do not remove the Paystack product/plan tables unless you are making a breaking major release.
|
|
100
|
+
|
|
101
|
+
### Use public Better Auth imports in package code
|
|
102
|
+
|
|
103
|
+
Runtime code should import from public Better Auth entrypoints:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { betterAuth } from "better-auth";
|
|
107
|
+
import { createAuthClient } from "better-auth/client";
|
|
108
|
+
import type { BetterAuthPluginDBSchema } from "better-auth/db";
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Do not add runtime imports from `@better-auth/core/*` in this package. Tests can use internals only if no public API covers the case.
|
|
112
|
+
|
|
113
|
+
## Common Mistakes
|
|
114
|
+
|
|
115
|
+
### Calling server-only helpers from the browser
|
|
116
|
+
|
|
117
|
+
Wrong:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { syncPaystackPlans } from "@alexasomba/better-auth-paystack";
|
|
121
|
+
|
|
122
|
+
await syncPaystackPlans(auth.$context, options);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Correct: call admin helpers from server jobs, cron handlers, CLI scripts, or trusted server routes only.
|
|
126
|
+
|
|
127
|
+
### Forgetting webhook secret normalization
|
|
128
|
+
|
|
129
|
+
Prefer the `webhook.secret` option:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
paystack({
|
|
133
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
134
|
+
webhook: {
|
|
135
|
+
secret: process.env.PAYSTACK_WEBHOOK_SECRET!,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`paystackWebhookSecret` is a compatibility alias. New code should not introduce it.
|
|
141
|
+
|
|
142
|
+
### Treating plans as just display data
|
|
143
|
+
|
|
144
|
+
Plans are used to validate billing operations and map Paystack plan codes. Include stable `name`, `amount`, `currency`, `interval`, `planCode`, and `paystackId` values when subscriptions are enabled.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: subscriptions-and-transactions
|
|
3
|
+
description: >
|
|
4
|
+
Build Paystack transaction and subscription flows with @alexasomba/better-auth-paystack. Use for initialize/verify transaction, create/upgrade/cancel/restore/list subscriptions, products/plans, billing portal links, webhooks, chargeSubscriptionRenewal, syncPaystackProducts, and syncPaystackPlans.
|
|
5
|
+
type: core
|
|
6
|
+
library: "@alexasomba/better-auth-paystack"
|
|
7
|
+
library_version: "2.4.1" # x-release-please-version
|
|
8
|
+
sources:
|
|
9
|
+
- "alexasomba/better-auth-paystack:README.md"
|
|
10
|
+
- "alexasomba/better-auth-paystack:src/routes.ts"
|
|
11
|
+
- "alexasomba/better-auth-paystack:src/operations.ts"
|
|
12
|
+
- "alexasomba/better-auth-paystack:src/client.ts"
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Enable subscriptions with concrete Paystack plan metadata:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { paystack } from "@alexasomba/better-auth-paystack";
|
|
21
|
+
|
|
22
|
+
paystack({
|
|
23
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
24
|
+
webhook: {
|
|
25
|
+
secret: process.env.PAYSTACK_WEBHOOK_SECRET!,
|
|
26
|
+
},
|
|
27
|
+
subscription: {
|
|
28
|
+
enabled: true,
|
|
29
|
+
plans: [
|
|
30
|
+
{
|
|
31
|
+
name: "starter",
|
|
32
|
+
amount: 250_000,
|
|
33
|
+
currency: "NGN",
|
|
34
|
+
interval: "monthly",
|
|
35
|
+
planCode: "PLN_starter",
|
|
36
|
+
paystackId: "1001",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "team",
|
|
40
|
+
amount: 1_000_000,
|
|
41
|
+
currency: "NGN",
|
|
42
|
+
interval: "monthly",
|
|
43
|
+
planCode: "PLN_team",
|
|
44
|
+
paystackId: "1002",
|
|
45
|
+
limits: {
|
|
46
|
+
seats: 10,
|
|
47
|
+
teams: 3,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Core Patterns
|
|
56
|
+
|
|
57
|
+
### Initialize and verify a transaction
|
|
58
|
+
|
|
59
|
+
Use the client plugin for browser-triggered checkout:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const initialized = await authClient.transaction.initialize({
|
|
63
|
+
amount: 250_000,
|
|
64
|
+
email: "customer@example.com",
|
|
65
|
+
currency: "NGN",
|
|
66
|
+
metadata: {
|
|
67
|
+
product: "starter-pack",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const verified = await authClient.transaction.verify({
|
|
72
|
+
reference: initialized.data.reference,
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Do not trust a redirect callback alone. Always verify the Paystack reference before granting access or updating billing state.
|
|
77
|
+
|
|
78
|
+
### Manage subscription lifecycle
|
|
79
|
+
|
|
80
|
+
Use canonical methods in new code:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
await authClient.subscription.create({
|
|
84
|
+
plan: "starter",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await authClient.subscription.upgrade({
|
|
88
|
+
plan: "team",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await authClient.subscription.cancel({
|
|
92
|
+
subscriptionId: "subscription_id",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await authClient.subscription.restore({
|
|
96
|
+
subscriptionId: "subscription_id",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const subscriptions = await authClient.subscription.list();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Deprecated aliases:
|
|
103
|
+
|
|
104
|
+
- `subscription.disable` maps to `subscription.cancel`
|
|
105
|
+
- `subscription.enable` maps to `subscription.restore`
|
|
106
|
+
|
|
107
|
+
Keep aliases only for compatibility tests or migration examples.
|
|
108
|
+
|
|
109
|
+
### Keep renewal and catalog sync server-side
|
|
110
|
+
|
|
111
|
+
These helpers are exported by the server package and are intentionally not client actions:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import {
|
|
115
|
+
chargeSubscriptionRenewal,
|
|
116
|
+
syncPaystackPlans,
|
|
117
|
+
syncPaystackProducts,
|
|
118
|
+
} from "@alexasomba/better-auth-paystack";
|
|
119
|
+
|
|
120
|
+
export async function runBillingJob(ctx: unknown, options: unknown) {
|
|
121
|
+
await syncPaystackProducts(ctx, options);
|
|
122
|
+
await syncPaystackPlans(ctx, options);
|
|
123
|
+
await chargeSubscriptionRenewal(ctx, options, {
|
|
124
|
+
subscriptionId: "subscription_id",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Use cron, background jobs, or trusted server functions. Do not call these from a browser component.
|
|
130
|
+
|
|
131
|
+
## Common Mistakes
|
|
132
|
+
|
|
133
|
+
### Mutating Paystack-managed subscriptions like local subscriptions
|
|
134
|
+
|
|
135
|
+
Seat-based or prorated subscription changes require locally managed subscription state. Paystack-managed subscriptions do not support every local mutation path.
|
|
136
|
+
|
|
137
|
+
Before implementing seat changes, check whether the target plan is local/seat-aware and whether the operation is supported by the helper being used.
|
|
138
|
+
|
|
139
|
+
### Skipping webhook verification
|
|
140
|
+
|
|
141
|
+
Configure `webhook.secret` and let the plugin verify incoming webhook payloads. Do not process Paystack webhook bodies through an unrelated JSON route that bypasses the plugin endpoint.
|
|
142
|
+
|
|
143
|
+
### Mixing plan name and Paystack plan code
|
|
144
|
+
|
|
145
|
+
Use `plan.name` for app-facing plan selection and `plan.planCode`/`paystackId` for Paystack identity. Do not send a Paystack plan code where the plugin expects the configured plan name.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-start
|
|
3
|
+
description: >
|
|
4
|
+
Integrate @alexasomba/better-auth-paystack in TanStack Start. Use for Better Auth API routes, tanstackStartCookies(), server functions, getRequestHeaders(), authClient Paystack actions, admin billing server functions, and Cloudflare Workers deployment.
|
|
5
|
+
type: composition
|
|
6
|
+
library: "@alexasomba/better-auth-paystack"
|
|
7
|
+
library_version: "2.4.1" # x-release-please-version
|
|
8
|
+
sources:
|
|
9
|
+
- "alexasomba/better-auth-paystack:examples/tanstack/README.md"
|
|
10
|
+
- "alexasomba/better-auth-paystack:examples/tanstack/src/lib/auth.ts"
|
|
11
|
+
- "alexasomba/better-auth-paystack:examples/tanstack/src/lib/auth-client.ts"
|
|
12
|
+
- "alexasomba/better-auth-paystack:examples/tanstack/src/routes/api/auth/$.ts"
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Create the Better Auth server config with Paystack and `tanstackStartCookies()` last:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { betterAuth } from "better-auth";
|
|
21
|
+
import { tanstackStartCookies } from "better-auth/tanstack-start";
|
|
22
|
+
import { paystack } from "@alexasomba/better-auth-paystack";
|
|
23
|
+
|
|
24
|
+
export const auth = betterAuth({
|
|
25
|
+
baseURL: process.env.BETTER_AUTH_URL,
|
|
26
|
+
plugins: [
|
|
27
|
+
paystack({
|
|
28
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
29
|
+
webhook: {
|
|
30
|
+
secret: process.env.PAYSTACK_WEBHOOK_SECRET!,
|
|
31
|
+
},
|
|
32
|
+
subscription: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
plans: [],
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
tanstackStartCookies(),
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Wire the catch-all auth route:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
46
|
+
import { auth } from "../../../lib/auth";
|
|
47
|
+
|
|
48
|
+
export const Route = createFileRoute("/api/auth/$")({
|
|
49
|
+
server: {
|
|
50
|
+
handlers: {
|
|
51
|
+
GET: ({ request }) => auth.handler(request),
|
|
52
|
+
POST: ({ request }) => auth.handler(request),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Create the client plugin:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { createAuthClient } from "better-auth/client";
|
|
62
|
+
import { paystackClient } from "@alexasomba/better-auth-paystack/client";
|
|
63
|
+
|
|
64
|
+
export const authClient = createAuthClient({
|
|
65
|
+
plugins: [paystackClient()],
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Core Patterns
|
|
70
|
+
|
|
71
|
+
### Use Paystack client actions in client components
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { authClient } from "../lib/auth-client";
|
|
75
|
+
|
|
76
|
+
export function SubscribeButton() {
|
|
77
|
+
return (
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={async () => {
|
|
81
|
+
await authClient.subscription.create({
|
|
82
|
+
plan: "starter",
|
|
83
|
+
});
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
Subscribe
|
|
87
|
+
</button>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use client actions for checkout and user-triggered subscription lifecycle calls. Do not import server helpers into React components.
|
|
93
|
+
|
|
94
|
+
### Use server functions for trusted billing operations
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
98
|
+
import { getRequestHeaders } from "@tanstack/react-start/server";
|
|
99
|
+
import { auth } from "./auth";
|
|
100
|
+
import { syncPaystackPlans } from "@alexasomba/better-auth-paystack";
|
|
101
|
+
|
|
102
|
+
export const syncPlans = createServerFn({ method: "POST" }).handler(async () => {
|
|
103
|
+
const session = await auth.api.getSession({
|
|
104
|
+
headers: await getRequestHeaders(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!session?.user) {
|
|
108
|
+
throw new Response("Unauthorized", { status: 401 });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await syncPaystackPlans(await auth.$context, {
|
|
112
|
+
secretKey: process.env.PAYSTACK_SECRET_KEY!,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return { ok: true };
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Pass request headers when Better Auth needs session context.
|
|
120
|
+
|
|
121
|
+
### Keep Cloudflare Worker dependencies compatible
|
|
122
|
+
|
|
123
|
+
The TanStack example deploys to Cloudflare Workers. Keep Better Auth companion packages on compatible versions. If `@better-auth/infra` pulls in `@better-auth/sso`, avoid mixing a beta SSO package with stable `better-auth`.
|
|
124
|
+
|
|
125
|
+
The known safe pin for this package version is:
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
overrides:
|
|
129
|
+
"@better-auth/sso": 1.6.9
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Common Mistakes
|
|
133
|
+
|
|
134
|
+
### Putting `tanstackStartCookies()` before Paystack
|
|
135
|
+
|
|
136
|
+
`tanstackStartCookies()` should be last in the Better Auth plugin array so cookie handling wraps the final auth behavior.
|
|
137
|
+
|
|
138
|
+
### Omitting auth headers in server functions
|
|
139
|
+
|
|
140
|
+
Wrong:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const session = await auth.api.getSession();
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Correct:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const session = await auth.api.getSession({
|
|
150
|
+
headers: await getRequestHeaders(),
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Debugging only root package builds
|
|
155
|
+
|
|
156
|
+
The root package can pass `vp pack` while the example Worker build fails. Reproduce Worker issues with:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
pnpm --filter ./examples/tanstack build
|
|
160
|
+
pnpm --filter ./examples/tanstack exec wrangler deploy --dry-run
|
|
161
|
+
```
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"version-C_50YiuM.mjs","names":[],"sources":["../src/version.ts"],"sourcesContent":["export const PACKAGE_VERSION = \"2.3.0\";\n"],"mappings":";AAAA,MAAa,kBAAkB"}
|