@deiondz/better-auth-razorpay 1.0.0

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 ADDED
@@ -0,0 +1,1201 @@
1
+ # Razorpay Plugin for Better Auth
2
+
3
+ A comprehensive subscription management plugin for Better Auth that integrates with Razorpay for handling recurring payments, subscriptions, and webhooks.
4
+
5
+ > **📚 Always consult [better-auth.com/docs](https://better-auth.com/docs) for the latest Better Auth API and best practices.**
6
+
7
+ ## Table of Contents
8
+
9
+ - [Overview](#overview)
10
+ - [Installation](#installation)
11
+ - [Configuration](#configuration)
12
+ - [Database Setup](#database-setup)
13
+ - [API Endpoints](#api-endpoints)
14
+ - [Client Usage](#client-usage)
15
+ - [Webhook Setup](#webhook-setup)
16
+ - [TypeScript Types](#typescript-types)
17
+ - [Error Handling](#error-handling)
18
+ - [Usage Examples](#usage-examples)
19
+ - [Best Practices](#best-practices)
20
+ - [Troubleshooting](#troubleshooting)
21
+
22
+ ## Overview
23
+
24
+ The Razorpay plugin provides a complete subscription management solution with the following features:
25
+
26
+ - ✅ **Subscription Management**: Create, pause, resume, and cancel subscriptions
27
+ - ✅ **Payment Verification**: Secure payment signature verification
28
+ - ✅ **Webhook Handling**: Automatic processing of Razorpay webhook events
29
+ - ✅ **Plan Management**: Retrieve and manage subscription plans
30
+ - ✅ **Type Safety**: Full TypeScript support with comprehensive type definitions
31
+ - ✅ **Error Handling**: Robust error handling with detailed error codes
32
+ - ✅ **Security**: Production-safe error messages and signature verification
33
+
34
+ ## Installation
35
+
36
+ ### Prerequisites
37
+
38
+ - Better Auth configured in your project
39
+ - Razorpay account with API credentials
40
+ - Plans created in Razorpay dashboard
41
+
42
+ ### Setup
43
+
44
+ 1. **Install the Package**
45
+
46
+ ```bash
47
+ npm install better-auth-razorpay
48
+ # or
49
+ yarn add better-auth-razorpay
50
+ # or
51
+ pnpm add better-auth-razorpay
52
+ # or
53
+ bun add better-auth-razorpay
54
+ ```
55
+
56
+ The package includes `razorpay` and `zod` as dependencies.
57
+
58
+ 2. **Configure the Plugin**
59
+
60
+ ```typescript
61
+ // src/lib/auth.ts (or your auth configuration file)
62
+ import { betterAuth } from 'better-auth'
63
+ import { razorpayPlugin } from 'better-auth-razorpay'
64
+
65
+ export const auth = betterAuth({
66
+ // ... your Better Auth configuration
67
+ database: mongodbAdapter(await connect()), // or your adapter
68
+ secret: process.env.BETTER_AUTH_SECRET,
69
+ baseURL: process.env.BETTER_AUTH_URL, // Optional if env var is set
70
+
71
+ plugins: [
72
+ razorpayPlugin({
73
+ keyId: process.env.RAZORPAY_KEY_ID!,
74
+ keySecret: process.env.RAZORPAY_KEY_SECRET!,
75
+ webhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
76
+ plans: [
77
+ 'plan_1234567890', // Basic Plan
78
+ 'plan_0987654321', // Premium Plan
79
+ ],
80
+ onWebhookEvent: async (payload, context) => {
81
+ // Optional: Custom webhook event handling
82
+ const { event, subscription, payment } = payload
83
+ const { userId, user } = context
84
+
85
+ // Send emails, update external systems, analytics, etc.
86
+ if (event === 'subscription.charged') {
87
+ await sendPaymentConfirmationEmail(user.email, {
88
+ amount: payment?.amount,
89
+ subscriptionId: subscription.id,
90
+ })
91
+ }
92
+ },
93
+ }),
94
+ ],
95
+ })
96
+ ```
97
+
98
+ 3. **Add Client Plugin**
99
+
100
+ ```typescript
101
+ // src/lib/auth-client.ts
102
+ import { createAuthClient } from 'better-auth/react'
103
+ import { razorpayClientPlugin } from 'better-auth-razorpay/client'
104
+ import type { auth } from './auth'
105
+
106
+ export const authClient = createAuthClient<typeof auth>({
107
+ baseURL: process.env.PUBLIC_URL, // Optional if same domain
108
+ plugins: [
109
+ razorpayClientPlugin(),
110
+ // ... other client plugins
111
+ ],
112
+ })
113
+ ```
114
+
115
+ 4. **Environment Variables**
116
+
117
+ Add to your `.env` file:
118
+
119
+ ```env
120
+ # Better Auth (if not using env vars)
121
+ BETTER_AUTH_SECRET=your_32_char_minimum_secret
122
+ BETTER_AUTH_URL=https://yourdomain.com
123
+
124
+ # Razorpay
125
+ RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxx
126
+ RAZORPAY_KEY_SECRET=your_secret_key
127
+ RAZORPAY_WEBHOOK_SECRET=your_webhook_secret
128
+ ```
129
+
130
+ 5. **Run Database Migration**
131
+
132
+ After adding the plugin, run the Better Auth CLI to apply schema changes:
133
+
134
+ ```bash
135
+ npx @better-auth/cli@latest migrate
136
+ # or for Prisma/Drizzle
137
+ npx @better-auth/cli@latest generate
138
+ ```
139
+
140
+ **Important:** Re-run the CLI after adding or changing plugins to update your database schema.
141
+
142
+ ## Configuration
143
+
144
+ ### Plugin Options
145
+
146
+ ```typescript
147
+ interface RazorpayPluginOptions {
148
+ keyId: string // Required: Razorpay Key ID
149
+ keySecret: string // Required: Razorpay Key Secret
150
+ webhookSecret?: string // Optional: Webhook secret for signature verification
151
+ plans: string[] // Required: Array of plan IDs from Razorpay dashboard
152
+ onWebhookEvent?: OnWebhookEventCallback // Optional: Custom webhook callback
153
+ }
154
+ ```
155
+
156
+ ### User Fields (Plug-and-Play)
157
+
158
+ The plugin extends the Better Auth user schema with subscription-related fields. When you add the plugin and run `npx @better-auth/cli@latest migrate` (or `generate`), these columns are added to your user table automatically:
159
+
160
+ - `subscriptionId`, `subscriptionPlanId`, `subscriptionStatus`, `subscriptionCurrentPeriodEnd`, `cancelAtPeriodEnd`, `lastPaymentDate`, `nextBillingDate`
161
+
162
+ You do **not** need to add them manually to `user.additionalFields` unless you prefer to define them yourself.
163
+
164
+ ## Database Setup
165
+
166
+ ### Automatic Schema Creation
167
+
168
+ The plugin automatically creates the following database models via Better Auth's schema system:
169
+
170
+ **`razorpaySubscription`**
171
+ - `userId` (string) - User ID
172
+ - `subscriptionId` (string) - Razorpay subscription ID
173
+ - `planId` (string) - Plan ID
174
+ - `status` (string) - Subscription status
175
+
176
+ **`razorpayCustomer`** (for future use)
177
+ - `userId` (string, unique) - User ID
178
+ - `razorpayCustomerId` (string, unique) - Razorpay customer ID
179
+
180
+ ### Database Adapters
181
+
182
+ The plugin works with all Better Auth database adapters:
183
+
184
+ - **MongoDB**: `mongodbAdapter()`
185
+ - **Prisma**: `prismaAdapter()`
186
+ - **Drizzle**: `drizzleAdapter()`
187
+ - **Direct connections**: PostgreSQL, MySQL, SQLite
188
+
189
+ **Important:** Better Auth uses adapter model names, NOT underlying table names. If your Prisma model is `User` mapping to table `users`, use the model name in configuration.
190
+
191
+ ## API Endpoints
192
+
193
+ All endpoints are prefixed with `/api/auth/razorpay/` (or your configured `basePath`). Endpoints use Better Auth's `createAuthEndpoint` and automatically handle authentication via `sessionMiddleware` where required.
194
+
195
+ ### 1. Get Plans
196
+
197
+ Retrieve all configured subscription plans.
198
+
199
+ **Endpoint:** `GET /api/auth/razorpay/get-plans`
200
+
201
+ **Authentication:** Not required (public endpoint)
202
+
203
+ **Response:**
204
+
205
+ ```typescript
206
+ {
207
+ success: true,
208
+ data: RazorpayPlan[]
209
+ }
210
+ ```
211
+
212
+ **Client Usage:**
213
+
214
+ ```typescript
215
+ import { authClient } from '@/lib/auth-client'
216
+
217
+ // Using Better Auth client
218
+ const response = await authClient.api.get('/razorpay/get-plans')
219
+ const { data } = response
220
+
221
+ // Or using fetch directly
222
+ const response = await fetch('/api/auth/razorpay/get-plans')
223
+ const { data } = await response.json()
224
+ ```
225
+
226
+ ---
227
+
228
+ ### 2. Subscribe
229
+
230
+ Create a new subscription for the authenticated user.
231
+
232
+ **Endpoint:** `POST /api/auth/razorpay/subscribe`
233
+
234
+ **Authentication:** Required (uses `sessionMiddleware`)
235
+
236
+ **Request Body:**
237
+
238
+ ```typescript
239
+ {
240
+ plan_id: string // Required: Plan ID from Razorpay
241
+ total_count?: number // Optional: Total billing cycles (default: 12)
242
+ quantity?: number // Optional: Quantity (default: 1)
243
+ start_at?: number // Optional: Start timestamp (Unix)
244
+ expire_by?: number // Optional: Expiry timestamp (Unix)
245
+ customer_notify?: boolean // Optional: Send notification (default: true)
246
+ addons?: Array<{ // Optional: Addons
247
+ item: {
248
+ name: string
249
+ amount: number
250
+ currency: string
251
+ }
252
+ }>
253
+ offer_id?: string // Optional: Offer ID
254
+ notes?: Record<string, string> // Optional: Custom notes
255
+ }
256
+ ```
257
+
258
+ **Response:**
259
+
260
+ ```typescript
261
+ {
262
+ success: true,
263
+ data: RazorpaySubscription
264
+ }
265
+ ```
266
+
267
+ **Client Usage:**
268
+
269
+ ```typescript
270
+ import { authClient } from '@/lib/auth-client'
271
+
272
+ // Using Better Auth client
273
+ const response = await authClient.api.post('/razorpay/subscribe', {
274
+ body: {
275
+ plan_id: 'plan_1234567890',
276
+ total_count: 12,
277
+ quantity: 1,
278
+ },
279
+ })
280
+
281
+ if (response.success) {
282
+ // Redirect to Razorpay checkout
283
+ window.location.href = response.data.short_url
284
+ }
285
+ ```
286
+
287
+ **Error Codes:**
288
+ - `PLAN_NOT_FOUND` - Plan ID not in configured plans
289
+ - `SUBSCRIPTION_ALREADY_EXISTS` - User already has an active subscription
290
+ - `UNAUTHORIZED` - User not authenticated
291
+ - `USER_NOT_FOUND` - User record not found
292
+
293
+ ---
294
+
295
+ ### 3. Get Subscription
296
+
297
+ Retrieve current subscription details for the authenticated user.
298
+
299
+ **Endpoint:** `GET /api/auth/razorpay/get-subscription`
300
+
301
+ **Authentication:** Required (uses `sessionMiddleware`)
302
+
303
+ **Response:**
304
+
305
+ ```typescript
306
+ {
307
+ success: true,
308
+ data: RazorpaySubscription | null // null if no subscription
309
+ }
310
+ ```
311
+
312
+ **Client Usage:**
313
+
314
+ ```typescript
315
+ import { authClient } from '@/lib/auth-client'
316
+
317
+ // Using Better Auth client
318
+ const response = await authClient.api.get('/razorpay/get-subscription')
319
+
320
+ if (response.success && response.data) {
321
+ console.log('Subscription status:', response.data.status)
322
+ console.log('Plan ID:', response.data.plan_id)
323
+ console.log('Cancel at period end:', response.data.cancel_at_period_end)
324
+ }
325
+ ```
326
+
327
+ **Error Codes:**
328
+ - `UNAUTHORIZED` - User not authenticated
329
+ - `USER_NOT_FOUND` - User record not found
330
+ - `SUBSCRIPTION_FETCH_FAILED` - Failed to fetch from Razorpay API
331
+
332
+ ---
333
+
334
+ ### 4. Verify Payment
335
+
336
+ Verify payment signature after Razorpay checkout completion.
337
+
338
+ **Endpoint:** `POST /api/auth/razorpay/verify-payment`
339
+
340
+ **Authentication:** Required (uses `sessionMiddleware`)
341
+
342
+ **Request Body:**
343
+
344
+ ```typescript
345
+ {
346
+ razorpay_payment_id: string // Required: Payment ID from Razorpay
347
+ razorpay_subscription_id: string // Required: Subscription ID from Razorpay
348
+ razorpay_signature: string // Required: Payment signature
349
+ }
350
+ ```
351
+
352
+ **Response:**
353
+
354
+ ```typescript
355
+ {
356
+ success: true,
357
+ data: {
358
+ message: 'Payment verified successfully',
359
+ payment_id: string,
360
+ subscription_id: string
361
+ }
362
+ }
363
+ ```
364
+
365
+ **Client Usage:**
366
+
367
+ ```typescript
368
+ import { authClient } from '@/lib/auth-client'
369
+
370
+ // After Razorpay checkout success callback
371
+ const handlePaymentSuccess = async (razorpayResponse: {
372
+ razorpay_payment_id: string
373
+ razorpay_subscription_id: string
374
+ razorpay_signature: string
375
+ }) => {
376
+ const response = await authClient.api.post('/razorpay/verify-payment', {
377
+ body: razorpayResponse,
378
+ })
379
+
380
+ if (response.success) {
381
+ console.log('Payment verified:', response.data.message)
382
+ // Redirect to success page
383
+ }
384
+ }
385
+ ```
386
+
387
+ **Error Codes:**
388
+ - `SIGNATURE_VERIFICATION_FAILED` - Invalid payment signature
389
+ - `UNAUTHORIZED` - User not authenticated
390
+ - `SUBSCRIPTION_NOT_FOUND` - Subscription record not found
391
+
392
+ ---
393
+
394
+ ### 5. Pause Subscription
395
+
396
+ Pause an active subscription.
397
+
398
+ **Endpoint:** `POST /api/auth/razorpay/pause-subscription`
399
+
400
+ **Authentication:** Required (uses `sessionMiddleware`)
401
+
402
+ **Request Body:**
403
+
404
+ ```typescript
405
+ {
406
+ subscription_id: string // Required: Subscription ID
407
+ }
408
+ ```
409
+
410
+ **Response:**
411
+
412
+ ```typescript
413
+ {
414
+ success: true,
415
+ data: RazorpaySubscription
416
+ }
417
+ ```
418
+
419
+ **Client Usage:**
420
+
421
+ ```typescript
422
+ import { authClient } from '@/lib/auth-client'
423
+
424
+ const response = await authClient.api.post('/razorpay/pause-subscription', {
425
+ body: {
426
+ subscription_id: 'sub_1234567890',
427
+ },
428
+ })
429
+
430
+ if (response.success) {
431
+ console.log('Subscription paused:', response.data.status)
432
+ }
433
+ ```
434
+
435
+ **Error Codes:**
436
+ - `UNAUTHORIZED` - User not authenticated or subscription doesn't belong to user
437
+ - `SUBSCRIPTION_NOT_FOUND` - Subscription not found
438
+
439
+ ---
440
+
441
+ ### 6. Resume Subscription
442
+
443
+ Resume a paused subscription.
444
+
445
+ **Endpoint:** `POST /api/auth/razorpay/resume-subscription`
446
+
447
+ **Authentication:** Required (uses `sessionMiddleware`)
448
+
449
+ **Request Body:**
450
+
451
+ ```typescript
452
+ {
453
+ subscription_id: string // Required: Subscription ID
454
+ }
455
+ ```
456
+
457
+ **Response:**
458
+
459
+ ```typescript
460
+ {
461
+ success: true,
462
+ data: RazorpaySubscription
463
+ }
464
+ ```
465
+
466
+ **Client Usage:**
467
+
468
+ ```typescript
469
+ import { authClient } from '@/lib/auth-client'
470
+
471
+ const response = await authClient.api.post('/razorpay/resume-subscription', {
472
+ body: {
473
+ subscription_id: 'sub_1234567890',
474
+ },
475
+ })
476
+
477
+ if (response.success) {
478
+ console.log('Subscription resumed:', response.data.status)
479
+ }
480
+ ```
481
+
482
+ **Error Codes:**
483
+ - `UNAUTHORIZED` - User not authenticated or subscription doesn't belong to user
484
+ - `SUBSCRIPTION_NOT_FOUND` - Subscription not found
485
+ - `INVALID_STATUS` - Subscription is not paused
486
+
487
+ ---
488
+
489
+ ### 7. Cancel Subscription
490
+
491
+ Cancel a subscription at the end of the current billing period.
492
+
493
+ **Endpoint:** `POST /api/auth/razorpay/cancel-subscription`
494
+
495
+ **Authentication:** Required (uses `sessionMiddleware`)
496
+
497
+ **Request Body:**
498
+
499
+ ```typescript
500
+ {
501
+ subscription_id: string // Required: Subscription ID
502
+ }
503
+ ```
504
+
505
+ **Response:**
506
+
507
+ ```typescript
508
+ {
509
+ success: true,
510
+ data: RazorpaySubscription
511
+ }
512
+ ```
513
+
514
+ **Note:** This cancels the subscription at period end, not immediately. The subscription remains active until the current billing period ends.
515
+
516
+ **Client Usage:**
517
+
518
+ ```typescript
519
+ import { authClient } from '@/lib/auth-client'
520
+
521
+ const response = await authClient.api.post('/razorpay/cancel-subscription', {
522
+ body: {
523
+ subscription_id: 'sub_1234567890',
524
+ },
525
+ })
526
+
527
+ if (response.success) {
528
+ console.log('Subscription will cancel at period end')
529
+ }
530
+ ```
531
+
532
+ **Error Codes:**
533
+ - `UNAUTHORIZED` - User not authenticated or subscription doesn't belong to user
534
+ - `SUBSCRIPTION_NOT_FOUND` - Subscription not found
535
+
536
+ ---
537
+
538
+ ### 8. Webhook
539
+
540
+ Handle Razorpay webhook events (automatically called by Razorpay).
541
+
542
+ **Endpoint:** `POST /api/auth/razorpay/webhook`
543
+
544
+ **Authentication:** Not required (webhook endpoint)
545
+
546
+ **Headers:**
547
+ - `x-razorpay-signature` - Webhook signature (required)
548
+
549
+ **Supported Events:**
550
+ - `subscription.authenticated` - Subscription authenticated
551
+ - `subscription.activated` - Subscription activated
552
+ - `subscription.charged` - Payment charged
553
+ - `subscription.cancelled` - Subscription cancelled
554
+ - `subscription.paused` - Subscription paused
555
+ - `subscription.resumed` - Subscription resumed
556
+ - `subscription.pending` - Subscription pending
557
+ - `subscription.halted` - Subscription halted
558
+
559
+ **Response:**
560
+
561
+ ```typescript
562
+ {
563
+ success: boolean
564
+ message?: string
565
+ }
566
+ ```
567
+
568
+ ## Client Usage
569
+
570
+ ### Better Auth Client Methods
571
+
572
+ The plugin integrates with Better Auth's client API. Use `authClient.api` for all endpoint calls:
573
+
574
+ ```typescript
575
+ import { authClient } from '@/lib/auth-client'
576
+
577
+ // GET request
578
+ const plans = await authClient.api.get('/razorpay/get-plans')
579
+
580
+ // POST request
581
+ const subscription = await authClient.api.post('/razorpay/subscribe', {
582
+ body: {
583
+ plan_id: 'plan_1234567890',
584
+ },
585
+ })
586
+ ```
587
+
588
+ ### Type Safety
589
+
590
+ Infer types from your auth configuration:
591
+
592
+ ```typescript
593
+ import type { auth } from '@/lib/auth'
594
+ import { createAuthClient } from 'better-auth/react'
595
+
596
+ export const authClient = createAuthClient<typeof auth>({
597
+ // ... config
598
+ })
599
+
600
+ // Infer session type
601
+ type Session = typeof authClient.$Infer.Session
602
+ type User = typeof authClient.$Infer.Session.user
603
+ ```
604
+
605
+ ## Webhook Setup
606
+
607
+ ### 1. Configure Webhook in Razorpay Dashboard
608
+
609
+ 1. Go to Razorpay Dashboard → Settings → Webhooks
610
+ 2. Add webhook URL: `https://yourdomain.com/api/auth/razorpay/webhook`
611
+ 3. Select events to subscribe:
612
+ - `subscription.authenticated`
613
+ - `subscription.activated`
614
+ - `subscription.charged`
615
+ - `subscription.cancelled`
616
+ - `subscription.paused`
617
+ - `subscription.resumed`
618
+ - `subscription.pending`
619
+ - `subscription.halted`
620
+ 4. Copy the webhook secret
621
+
622
+ ### 2. Configure Webhook Secret
623
+
624
+ Add the webhook secret to your environment variables:
625
+
626
+ ```env
627
+ RAZORPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx
628
+ ```
629
+
630
+ ### 3. Custom Webhook Handler (Optional)
631
+
632
+ You can provide a custom callback function to handle webhook events:
633
+
634
+ ```typescript
635
+ razorpayPlugin({
636
+ // ... other options
637
+ onWebhookEvent: async (payload, context) => {
638
+ const { event, subscription, payment } = payload
639
+ const { userId, user } = context
640
+
641
+ switch (event) {
642
+ case 'subscription.charged':
643
+ // Send payment confirmation email
644
+ await sendEmail(user.email, 'Payment Successful', {
645
+ amount: payment?.amount,
646
+ subscriptionId: subscription.id,
647
+ })
648
+ break
649
+
650
+ case 'subscription.cancelled':
651
+ // Send cancellation email
652
+ await sendEmail(user.email, 'Subscription Cancelled')
653
+ break
654
+
655
+ case 'subscription.activated':
656
+ // Update external systems
657
+ await updateCRM(userId, { subscriptionActive: true })
658
+ break
659
+
660
+ // Handle other events...
661
+ }
662
+ },
663
+ })
664
+ ```
665
+
666
+ **Important:** Webhook callback errors are handled silently and don't break core webhook processing. The callback is for custom business logic only.
667
+
668
+ ## TypeScript Types
669
+
670
+ ### Import Types
671
+
672
+ ```typescript
673
+ import type {
674
+ RazorpaySubscription,
675
+ RazorpaySubscriptionRecord,
676
+ RazorpayUserRecord,
677
+ RazorpayApiResponse,
678
+ RazorpaySuccessResponse,
679
+ RazorpayErrorResponse,
680
+ RazorpayPluginOptions,
681
+ OnWebhookEventCallback,
682
+ } from 'better-auth-razorpay'
683
+ ```
684
+
685
+ ### Response Types
686
+
687
+ ```typescript
688
+ // Success response
689
+ interface RazorpaySuccessResponse<T> {
690
+ success: true
691
+ data: T
692
+ }
693
+
694
+ // Error response
695
+ interface RazorpayErrorResponse {
696
+ success: false
697
+ error: {
698
+ code: string
699
+ description: string
700
+ [key: string]: unknown // Additional error metadata (development only)
701
+ }
702
+ }
703
+
704
+ // Union type
705
+ type RazorpayApiResponse<T> = RazorpaySuccessResponse<T> | RazorpayErrorResponse
706
+ ```
707
+
708
+ ### Subscription Type
709
+
710
+ ```typescript
711
+ interface RazorpaySubscription {
712
+ id: string
713
+ entity: string
714
+ plan_id: string
715
+ status: string
716
+ current_start: number
717
+ current_end: number
718
+ ended_at: number | null
719
+ quantity: number
720
+ notes: Record<string, string> | null
721
+ charge_at: number
722
+ start_at: number
723
+ end_at: number
724
+ auth_attempts: number
725
+ total_count: number
726
+ paid_count: number
727
+ customer_notify: boolean
728
+ created_at: number
729
+ expire_by: number | null
730
+ short_url: string
731
+ has_scheduled_changes: boolean
732
+ change_scheduled_at: number | null
733
+ source: string
734
+ offer_id: string | null
735
+ remaining_count: string
736
+ }
737
+ ```
738
+
739
+ ## Error Handling
740
+
741
+ ### Error Response Format
742
+
743
+ All endpoints return errors in a consistent format:
744
+
745
+ ```typescript
746
+ {
747
+ success: false,
748
+ error: {
749
+ code: string, // Error code (e.g., 'UNAUTHORIZED', 'PLAN_NOT_FOUND')
750
+ description: string, // Human-readable error message
751
+ [key: string]: unknown // Additional error metadata (development only)
752
+ }
753
+ }
754
+ ```
755
+
756
+ ### Common Error Codes
757
+
758
+ | Code | Description |
759
+ |------|-------------|
760
+ | `INVALID_REQUEST` | Validation error (Zod schema validation failed) |
761
+ | `UNAUTHORIZED` | User not authenticated or subscription doesn't belong to user |
762
+ | `PLAN_NOT_FOUND` | Plan ID not found in configured plans |
763
+ | `SUBSCRIPTION_NOT_FOUND` | Subscription record not found |
764
+ | `SUBSCRIPTION_ALREADY_EXISTS` | User already has an active subscription |
765
+ | `INVALID_STATUS` | Subscription is not in the expected status |
766
+ | `SIGNATURE_VERIFICATION_FAILED` | Payment signature verification failed |
767
+ | `NETWORK_ERROR` | Network connection failed |
768
+ | `TIMEOUT_ERROR` | Request timed out |
769
+ | `RAZORPAY_ERROR` | Razorpay API error |
770
+ | `UNKNOWN_ERROR` | Unexpected error occurred |
771
+
772
+ ### Error Handling Example
773
+
774
+ ```typescript
775
+ import { authClient } from '@/lib/auth-client'
776
+
777
+ try {
778
+ const response = await authClient.api.post('/razorpay/subscribe', {
779
+ body: { plan_id: 'plan_123' },
780
+ })
781
+
782
+ if (!response.success) {
783
+ switch (response.error.code) {
784
+ case 'PLAN_NOT_FOUND':
785
+ toast.error('Plan not available')
786
+ break
787
+ case 'SUBSCRIPTION_ALREADY_EXISTS':
788
+ toast.error('You already have an active subscription')
789
+ break
790
+ default:
791
+ toast.error(response.error.description)
792
+ }
793
+ return
794
+ }
795
+
796
+ // Handle success
797
+ window.location.href = response.data.short_url
798
+ } catch (error) {
799
+ console.error('Network error:', error)
800
+ toast.error('Network error. Please try again.')
801
+ }
802
+ ```
803
+
804
+ ## Usage Examples
805
+
806
+ ### Complete Subscription Flow
807
+
808
+ ```typescript
809
+ import { authClient } from '@/lib/auth-client'
810
+
811
+ async function handleSubscriptionFlow() {
812
+ // 1. Get available plans
813
+ const plansResponse = await authClient.api.get('/razorpay/get-plans')
814
+ if (!plansResponse.success) {
815
+ console.error('Failed to fetch plans')
816
+ return
817
+ }
818
+
819
+ const plans = plansResponse.data
820
+ const selectedPlan = plans[0]
821
+
822
+ // 2. Create subscription
823
+ const subscribeResponse = await authClient.api.post('/razorpay/subscribe', {
824
+ body: {
825
+ plan_id: selectedPlan.id,
826
+ total_count: 12,
827
+ },
828
+ })
829
+
830
+ if (!subscribeResponse.success) {
831
+ console.error('Failed to create subscription:', subscribeResponse.error)
832
+ return
833
+ }
834
+
835
+ // 3. Redirect to Razorpay checkout
836
+ window.location.href = subscribeResponse.data.short_url
837
+
838
+ // 4. After payment, verify payment (in Razorpay success handler)
839
+ // This is handled in the Razorpay checkout callback
840
+ }
841
+
842
+ // Razorpay checkout success handler
843
+ function handleRazorpaySuccess(response: {
844
+ razorpay_payment_id: string
845
+ razorpay_subscription_id: string
846
+ razorpay_signature: string
847
+ }) {
848
+ authClient.api.post('/razorpay/verify-payment', {
849
+ body: response,
850
+ }).then((result) => {
851
+ if (result.success) {
852
+ // Redirect to success page
853
+ window.location.href = '/subscription/success'
854
+ }
855
+ })
856
+ }
857
+ ```
858
+
859
+ ### React Hook Example with TanStack Query
860
+
861
+ ```typescript
862
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
863
+ import { authClient } from '@/lib/auth-client'
864
+
865
+ // Get plans
866
+ export function usePlans() {
867
+ return useQuery({
868
+ queryKey: ['razorpay', 'plans'],
869
+ queryFn: async () => {
870
+ const response = await authClient.api.get('/razorpay/get-plans')
871
+ if (!response.success) throw new Error(response.error.description)
872
+ return response.data
873
+ },
874
+ })
875
+ }
876
+
877
+ // Get subscription
878
+ export function useSubscription() {
879
+ return useQuery({
880
+ queryKey: ['razorpay', 'subscription'],
881
+ queryFn: async () => {
882
+ const response = await authClient.api.get('/razorpay/get-subscription')
883
+ if (!response.success) throw new Error(response.error.description)
884
+ return response.data
885
+ },
886
+ })
887
+ }
888
+
889
+ // Subscribe
890
+ export function useSubscribe() {
891
+ const queryClient = useQueryClient()
892
+
893
+ return useMutation({
894
+ mutationFn: async (planId: string) => {
895
+ const response = await authClient.api.post('/razorpay/subscribe', {
896
+ body: { plan_id: planId },
897
+ })
898
+ if (!response.success) throw new Error(response.error.description)
899
+ return response.data
900
+ },
901
+ onSuccess: () => {
902
+ queryClient.invalidateQueries({ queryKey: ['razorpay', 'subscription'] })
903
+ },
904
+ })
905
+ }
906
+
907
+ // Cancel subscription
908
+ export function useCancelSubscription() {
909
+ const queryClient = useQueryClient()
910
+
911
+ return useMutation({
912
+ mutationFn: async (subscriptionId: string) => {
913
+ const response = await authClient.api.post('/razorpay/cancel-subscription', {
914
+ body: { subscription_id: subscriptionId },
915
+ })
916
+ if (!response.success) throw new Error(response.error.description)
917
+ return response.data
918
+ },
919
+ onSuccess: () => {
920
+ queryClient.invalidateQueries({ queryKey: ['razorpay', 'subscription'] })
921
+ },
922
+ })
923
+ }
924
+
925
+ // Pause subscription
926
+ export function usePauseSubscription() {
927
+ const queryClient = useQueryClient()
928
+
929
+ return useMutation({
930
+ mutationFn: async (subscriptionId: string) => {
931
+ const response = await authClient.api.post('/razorpay/pause-subscription', {
932
+ body: { subscription_id: subscriptionId },
933
+ })
934
+ if (!response.success) throw new Error(response.error.description)
935
+ return response.data
936
+ },
937
+ onSuccess: () => {
938
+ queryClient.invalidateQueries({ queryKey: ['razorpay', 'subscription'] })
939
+ },
940
+ })
941
+ }
942
+
943
+ // Resume subscription
944
+ export function useResumeSubscription() {
945
+ const queryClient = useQueryClient()
946
+
947
+ return useMutation({
948
+ mutationFn: async (subscriptionId: string) => {
949
+ const response = await authClient.api.post('/razorpay/resume-subscription', {
950
+ body: { subscription_id: subscriptionId },
951
+ })
952
+ if (!response.success) throw new Error(response.error.description)
953
+ return response.data
954
+ },
955
+ onSuccess: () => {
956
+ queryClient.invalidateQueries({ queryKey: ['razorpay', 'subscription'] })
957
+ },
958
+ })
959
+ }
960
+ ```
961
+
962
+ ### React Component Example
963
+
964
+ ```typescript
965
+ 'use client'
966
+
967
+ import { usePlans, useSubscription, useSubscribe, useCancelSubscription } from '@/hooks/use-razorpay'
968
+
969
+ export function SubscriptionPage() {
970
+ const { data: plans, isLoading: plansLoading } = usePlans()
971
+ const { data: subscription, isLoading: subLoading } = useSubscription()
972
+ const subscribe = useSubscribe()
973
+ const cancel = useCancelSubscription()
974
+
975
+ const handleSubscribe = async (planId: string) => {
976
+ try {
977
+ const subscription = await subscribe.mutateAsync(planId)
978
+ // Redirect to Razorpay checkout
979
+ window.location.href = subscription.short_url
980
+ } catch (error) {
981
+ console.error('Failed to create subscription:', error)
982
+ // Handle error (show toast, etc.)
983
+ }
984
+ }
985
+
986
+ const handleCancel = async () => {
987
+ if (!subscription) return
988
+ try {
989
+ await cancel.mutateAsync(subscription.id)
990
+ alert('Subscription will be cancelled at period end')
991
+ } catch (error) {
992
+ console.error('Failed to cancel subscription:', error)
993
+ }
994
+ }
995
+
996
+ if (plansLoading || subLoading) {
997
+ return <div>Loading...</div>
998
+ }
999
+
1000
+ return (
1001
+ <div>
1002
+ {subscription ? (
1003
+ <div>
1004
+ <h2>Current Subscription</h2>
1005
+ <p>Status: {subscription.status}</p>
1006
+ <p>Plan: {subscription.plan_id}</p>
1007
+ {subscription.cancel_at_period_end && (
1008
+ <p className="text-warning">Will cancel at period end</p>
1009
+ )}
1010
+ <button
1011
+ onClick={handleCancel}
1012
+ disabled={cancel.isPending}
1013
+ >
1014
+ {cancel.isPending ? 'Cancelling...' : 'Cancel Subscription'}
1015
+ </button>
1016
+ </div>
1017
+ ) : (
1018
+ <div>
1019
+ <h2>Select a Plan</h2>
1020
+ {plans?.map((plan) => (
1021
+ <div key={plan.id}>
1022
+ <h3>{plan.name || plan.id}</h3>
1023
+ <p>₹{plan.amount / 100}/month</p>
1024
+ <button
1025
+ onClick={() => handleSubscribe(plan.id)}
1026
+ disabled={subscribe.isPending}
1027
+ >
1028
+ {subscribe.isPending ? 'Processing...' : 'Subscribe'}
1029
+ </button>
1030
+ </div>
1031
+ ))}
1032
+ </div>
1033
+ )}
1034
+ </div>
1035
+ )
1036
+ }
1037
+ ```
1038
+
1039
+ ### Razorpay Checkout Integration
1040
+
1041
+ ```typescript
1042
+ import { loadScript } from '@/lib/razorpay-script' // Your script loader
1043
+
1044
+ async function initializeRazorpayCheckout(subscriptionId: string) {
1045
+ // Load Razorpay script
1046
+ await loadScript('https://checkout.razorpay.com/v1/checkout.js')
1047
+
1048
+ const options = {
1049
+ subscription_id: subscriptionId,
1050
+ key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID!,
1051
+ name: 'Your App Name',
1052
+ description: 'Subscription Payment',
1053
+ handler: async function (response: {
1054
+ razorpay_payment_id: string
1055
+ razorpay_subscription_id: string
1056
+ razorpay_signature: string
1057
+ }) {
1058
+ // Verify payment
1059
+ const verifyResponse = await authClient.api.post('/razorpay/verify-payment', {
1060
+ body: response,
1061
+ })
1062
+
1063
+ if (verifyResponse.success) {
1064
+ // Redirect to success page
1065
+ window.location.href = '/subscription/success'
1066
+ } else {
1067
+ // Handle error
1068
+ console.error('Payment verification failed:', verifyResponse.error)
1069
+ }
1070
+ },
1071
+ prefill: {
1072
+ name: user.name,
1073
+ email: user.email,
1074
+ },
1075
+ theme: {
1076
+ color: '#3399cc',
1077
+ },
1078
+ }
1079
+
1080
+ const razorpay = new (window as any).Razorpay(options)
1081
+ razorpay.open()
1082
+ }
1083
+ ```
1084
+
1085
+ ## Best Practices
1086
+
1087
+ ### 1. Better Auth Configuration
1088
+
1089
+ - **Use environment variables** - Prefer `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` env vars over config
1090
+ - **Re-run CLI after changes** - Always run `npx @better-auth/cli@latest migrate` after adding/changing plugins
1091
+ - **Model names vs table names** - Use ORM model names in config, not DB table names
1092
+ - **Type inference** - Use `typeof auth.$Infer.Session` for type safety
1093
+
1094
+ ### 2. Plan Management
1095
+
1096
+ - **Create plans in Razorpay dashboard** - Plans are managed in Razorpay, not through the API
1097
+ - **Use plan IDs in configuration** - Add all plan IDs to the `plans` array in plugin options
1098
+ - **Validate plans client-side** - Always validate plan IDs before allowing users to subscribe
1099
+
1100
+ ### 3. Security
1101
+
1102
+ - **Always verify payment signatures** - Never skip signature verification
1103
+ - **Use HTTPS** - Always use HTTPS in production
1104
+ - **Protect webhook secret** - Never expose webhook secret in client-side code
1105
+ - **Validate user ownership** - The plugin automatically validates subscription ownership
1106
+ - **Production error messages** - Error messages are automatically sanitized in production
1107
+
1108
+ ### 4. Error Handling
1109
+
1110
+ - **Handle all error codes** - Check for specific error codes and provide user-friendly messages
1111
+ - **Log errors server-side** - Log detailed errors server-side, show generic messages to users
1112
+ - **Retry logic** - Implement retry logic for network errors and timeouts
1113
+ - **Use Better Auth error patterns** - Follow Better Auth's error handling conventions
1114
+
1115
+ ### 5. Webhook Handling
1116
+
1117
+ - **Idempotent operations** - Ensure webhook handlers are idempotent
1118
+ - **Handle failures gracefully** - Webhook callback errors don't break core functionality
1119
+ - **Monitor webhook events** - Log webhook events for debugging and analytics
1120
+ - **Use webhook callback** - Leverage `onWebhookEvent` for custom business logic
1121
+
1122
+ ### 6. Subscription Lifecycle
1123
+
1124
+ - **Check subscription status** - Always check subscription status before allowing actions
1125
+ - **Handle edge cases** - Account for paused, cancelled, and expired subscriptions
1126
+ - **Update UI accordingly** - Reflect subscription status changes in your UI
1127
+ - **Use webhooks for updates** - Rely on webhooks for status updates rather than polling
1128
+
1129
+ ### 7. Performance
1130
+
1131
+ - **Cache plans** - Plans don't change frequently, consider caching
1132
+ - **Optimize queries** - The plugin already optimizes database queries
1133
+ - **Use webhooks** - Rely on webhooks for status updates rather than polling
1134
+ - **TanStack Query** - Use TanStack Query for client-side caching and state management
1135
+
1136
+ ### 8. TypeScript Best Practices
1137
+
1138
+ - **Infer types from auth** - Use `typeof auth.$Infer.Session` for type safety
1139
+ - **Type client properly** - Use `createAuthClient<typeof auth>()` for full type inference
1140
+ - **Export types** - Export and reuse types from the plugin
1141
+
1142
+ ## Troubleshooting
1143
+
1144
+ ### Common Issues
1145
+
1146
+ **1. "Plan not found in configured plans"**
1147
+ - Ensure the plan ID exists in Razorpay dashboard
1148
+ - Add the plan ID to the `plans` array in plugin configuration
1149
+ - Re-run Better Auth CLI after updating plans
1150
+
1151
+ **2. "Webhook signature verification failed"**
1152
+ - Verify webhook secret matches Razorpay dashboard
1153
+ - Ensure webhook URL is correct: `https://yourdomain.com/api/auth/razorpay/webhook`
1154
+ - Check that request body is not modified
1155
+ - Verify `x-razorpay-signature` header is present
1156
+
1157
+ **3. "Subscription already exists"**
1158
+ - User already has an active subscription
1159
+ - Cancel or pause existing subscription first
1160
+ - Check subscription status before creating new one
1161
+
1162
+ **4. "User not authenticated"**
1163
+ - Ensure user is logged in via Better Auth
1164
+ - Check session middleware is properly configured
1165
+ - Verify `sessionMiddleware` is used in endpoint configuration
1166
+
1167
+ **5. "Subscription not found"**
1168
+ - Subscription may have been deleted
1169
+ - Check subscription ID is correct
1170
+ - Verify subscription belongs to the user
1171
+
1172
+ **6. Database Schema Issues**
1173
+ - Run `npx @better-auth/cli@latest migrate` after adding plugin
1174
+ - For Prisma/Drizzle: Run `npx @better-auth/cli@latest generate`
1175
+ - Check that user additional fields are properly configured
1176
+
1177
+ **7. Type Errors**
1178
+ - Ensure you're using `createAuthClient<typeof auth>()` for type inference
1179
+ - Import types from `better-auth-razorpay`
1180
+ - Check that plugin is properly exported
1181
+
1182
+ ## Resources
1183
+
1184
+ - [Better Auth Documentation](https://better-auth.com/docs)
1185
+ - [Better Auth Options Reference](https://better-auth.com/docs/reference/options)
1186
+ - [Better Auth LLMs.txt](https://better-auth.com/llms.txt)
1187
+ - [Razorpay API Documentation](https://razorpay.com/docs/api/)
1188
+ - [Razorpay Subscriptions Guide](https://razorpay.com/docs/payments/subscriptions/)
1189
+
1190
+ ## Support
1191
+
1192
+ For issues, questions, or contributions:
1193
+
1194
+ 1. Check the [Better Auth documentation](https://better-auth.com/docs)
1195
+ 2. Review [Razorpay API documentation](https://razorpay.com/docs/api/)
1196
+ 3. Open an issue on GitHub
1197
+
1198
+ ## License
1199
+
1200
+ This plugin is part of the Better Auth ecosystem and follows the same license as Better Auth.
1201
+