@spaire/better-auth 2.0.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.
Files changed (2) hide show
  1. package/README.md +495 -0
  2. package/package.json +69 -0
package/README.md ADDED
@@ -0,0 +1,495 @@
1
+ # @spaire/better-auth
2
+
3
+ A [Better Auth](https://github.com/better-auth/better-auth) plugin for integrating [Spaire](https://spairehq.com) payments and subscriptions into your authentication flow.
4
+
5
+ ## Features
6
+
7
+ - Checkout Integration
8
+ - Customer Portal
9
+ - Automatic Customer creation on signup
10
+ - Event Ingestion & Customer Meters for flexible Usage Based Billing
11
+ - Handle Spaire Webhooks securely with signature verification
12
+ - Reference System to associate purchases with organizations
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm add better-auth @spaire/better-auth @spaire/sdk
18
+ ```
19
+
20
+ ## Preparation
21
+
22
+ Go to your Spaire Organization Settings, and create an Organization Access Token. Add it to your environment.
23
+
24
+ ```bash
25
+ # .env
26
+ SPAIRE_ACCESS_TOKEN=...
27
+ ```
28
+
29
+ ### Configuring BetterAuth Server
30
+
31
+ The Spaire plugin comes with a handful additional plugins which adds functionality to your stack.
32
+
33
+ - Checkout - Enables a seamless checkout integration
34
+ - Portal - Makes it possible for your customers to manage their orders, subscriptions & granted benefits
35
+ - Usage - Simple extension for listing customer meters & ingesting events for Usage Based Billing
36
+ - Webhooks - Listen for relevant Spaire webhooks
37
+
38
+ ```typescript
39
+ import { betterAuth } from "better-auth";
40
+ import { spaire, checkout, portal, usage, webhooks } from "@spaire/better-auth";
41
+ import { Spaire } from "@spaire/sdk";
42
+
43
+ const spaireClient = new Spaire({
44
+ accessToken: process.env.SPAIRE_ACCESS_TOKEN,
45
+ // Use 'sandbox' if you're using the Spaire Sandbox environment
46
+ // Remember that access tokens, products, etc. are completely separated between environments.
47
+ // Access tokens obtained in Production are for instance not usable in the Sandbox environment.
48
+ server: 'sandbox'
49
+ });
50
+
51
+ const auth = betterAuth({
52
+ // ... Better Auth config
53
+ plugins: [
54
+ spaire({
55
+ client: spaireClient,
56
+ createCustomerOnSignUp: true,
57
+ use: [
58
+ checkout({
59
+ products: [
60
+ {
61
+ productId: "123-456-789", // ID of Product from Spaire Dashboard
62
+ slug: "pro" // Custom slug for easy reference in Checkout URL, e.g. /checkout/pro
63
+ }
64
+ ],
65
+ successUrl: "/success?checkout_id={CHECKOUT_ID}",
66
+ authenticatedUsersOnly: true,
67
+ returnUrl: "https://myapp.com", // Optional Return URL, which renders a Back-button in the Checkout
68
+ }),
69
+ portal({
70
+ returnUrl: "https://myapp.com", // Optional Return URL, which renders a Back-button in the Customer Portal
71
+ }),
72
+ usage(),
73
+ webhooks({
74
+ secret: process.env.SPAIRE_WEBHOOK_SECRET,
75
+ onCustomerStateChanged: (payload) => // Triggered when anything regarding a customer changes
76
+ onOrderPaid: (payload) => // Triggered when an order was paid (purchase, subscription renewal, etc.)
77
+ ... // Over 25 granular webhook handlers
78
+ onPayload: (payload) => // Catch-all for all events
79
+ })
80
+ ],
81
+ })
82
+ ]
83
+ });
84
+ ```
85
+
86
+ ### Configuring BetterAuth Client
87
+
88
+ You will be using the BetterAuth Client to interact with the Spaire functionalities.
89
+
90
+ ```typescript
91
+ import { createAuthClient } from "better-auth/react";
92
+ import { spaireClient } from "@spaire/better-auth/client";
93
+ import { organizationClient } from "better-auth/client/plugins";
94
+
95
+ // This is all that is needed
96
+ // All Spaire plugins, etc. should be attached to the server-side BetterAuth config
97
+ export const authClient = createAuthClient({
98
+ plugins: [spaireClient()],
99
+ });
100
+ ```
101
+
102
+ ## Configuration Options
103
+
104
+ ```typescript
105
+ import { betterAuth } from "better-auth";
106
+ import {
107
+ spaire,
108
+ checkout,
109
+ portal,
110
+ usage,
111
+ webhooks,
112
+ } from "@spaire/better-auth";
113
+ import { Spaire } from "@spaire/sdk";
114
+
115
+ const spaireClient = new Spaire({
116
+ accessToken: process.env.SPAIRE_ACCESS_TOKEN,
117
+ // Use 'sandbox' if you're using the Spaire Sandbox environment
118
+ // Remember that access tokens, products, etc. are completely separated between environments.
119
+ // Access tokens obtained in Production are for instance not usable in the Sandbox environment.
120
+ server: "sandbox",
121
+ });
122
+
123
+ const auth = betterAuth({
124
+ // ... Better Auth config
125
+ plugins: [
126
+ spaire({
127
+ client: spaireClient,
128
+ createCustomerOnSignUp: true,
129
+ getCustomerCreateParams: ({ user }, request) => ({
130
+ metadata: {
131
+ myCustomProperty: 123,
132
+ },
133
+ }),
134
+ use: [
135
+ // This is where you add Spaire plugins
136
+ ],
137
+ }),
138
+ ],
139
+ });
140
+ ```
141
+
142
+ ### Required Options
143
+
144
+ - `client`: Spaire SDK client instance
145
+
146
+ ### Optional Options
147
+
148
+ - `createCustomerOnSignUp`: Automatically create a Spaire customer when a user signs up
149
+ - `getCustomerCreateParams`: Custom function to provide additional customer creation metadata
150
+
151
+ ### Customers
152
+
153
+ When `createCustomerOnSignUp` is enabled, a new Spaire Customer is automatically created when a new User is added in the Better-Auth Database.
154
+
155
+ All new customers are created with an associated `externalId`, which is the ID of your User in the Database. This allows us to skip any Spaire <-> User mapping in your Database.
156
+
157
+ ## Checkout Plugin
158
+
159
+ To support checkouts in your app, simply pass the Checkout plugin to the use-property.
160
+
161
+ ```typescript
162
+ import { spaire, checkout } from "@spaire/better-auth";
163
+
164
+ const auth = betterAuth({
165
+ // ... Better Auth config
166
+ plugins: [
167
+ spaire({
168
+ ...
169
+ use: [
170
+ checkout({
171
+ // Optional field - will make it possible to pass a slug to checkout instead of Product ID
172
+ products: [ { productId: "123-456-789", slug: "pro" } ],
173
+ // Relative URL to return to when checkout is successfully completed
174
+ successUrl: "/success?checkout_id={CHECKOUT_ID}",
175
+ // Optional Return URL, which renders a Back-button in the Checkout
176
+ returnUrl: "https://myapp.com",
177
+ // Wheather you want to allow unauthenticated checkout sessions or not
178
+ authenticatedUsersOnly: true,
179
+ // Enforces the theme - System-preferred theme will be set if left omitted
180
+ theme: "dark"
181
+ })
182
+ ],
183
+ })
184
+ ]
185
+ });
186
+ ```
187
+
188
+ When checkouts are enabled, you're able to initialize Checkout Sessions using the checkout-method on the BetterAuth Client. This will redirect the user to the Product Checkout.
189
+
190
+ ```typescript
191
+ await authClient.checkout({
192
+ // Any Spaire Product ID can be passed here
193
+ products: ["e651f46d-ac20-4f26-b769-ad088b123df2"],
194
+ // Or, if you setup "products" in the Checkout Config, you can pass the slug
195
+ slug: "pro",
196
+ });
197
+ ```
198
+
199
+ Checkouts will automatically carry the authenticated User as the customer to the checkout. Email-address will be "locked-in".
200
+
201
+ If `authenticatedUsersOnly` is `false` - then it will be possible to trigger checkout sessions without any associated customer.
202
+
203
+
204
+ ### Checkout Embed
205
+
206
+ You can use the `checkoutEmbed` method to instead open the Checkout as an Embed on your site.
207
+
208
+ ```typescript
209
+ const embed = await authClient.checkoutEmbed({
210
+ products: ["e651f46d-ac20-4f26-b769-ad088b123df2"],
211
+ });
212
+
213
+ // Listen for successful completion
214
+ checkout.addEventListener("success", (event) => {
215
+ console.log("Purchase successful!", event.detail);
216
+
217
+ // Call event.preventDefault() if you want to prevent the standard behavior
218
+ // event.preventDefault()
219
+ // Note: For success event, this prevents automatic redirection if redirect is true
220
+
221
+ // If redirect is false, you can show your own success message
222
+ if (!event.detail.redirect) {
223
+ showSuccessMessage();
224
+ }
225
+ // Otherwise, the user will be redirected to the success URL (unless prevented)
226
+ });
227
+ ```
228
+
229
+ ### Organization Support
230
+
231
+ This plugin supports the Organization plugin. If you pass the organization ID to the Checkout referenceId, you will be able to keep track of purchases made from organization members.
232
+
233
+ ```typescript
234
+ const organizationId = (await authClient.organization.list())?.data?.[0]?.id,
235
+
236
+ await authClient.checkout({
237
+ // Any Spaire Product ID can be passed here
238
+ products: ["e651f46d-ac20-4f26-b769-ad088b123df2"],
239
+ // Or, if you setup "products" in the Checkout Config, you can pass the slug
240
+ slug: 'pro',
241
+ // Reference ID will be saved as `referenceId` in the metadata of the checkout, order & subscription object
242
+ referenceId: organizationId
243
+ });
244
+ ```
245
+
246
+ ## Portal Plugin
247
+
248
+ A plugin which enables customer management of their purchases, orders and subscriptions.
249
+
250
+ ```typescript
251
+ import { spaire, checkout, portal } from "@spaire/better-auth";
252
+
253
+ const auth = betterAuth({
254
+ // ... Better Auth config
255
+ plugins: [
256
+ spaire({
257
+ ...
258
+ use: [
259
+ checkout(...),
260
+ portal({
261
+ // Optional Return URL, which renders a Back-button in the Customer Portal
262
+ redirectUrl: "https://myapp.com"
263
+ })
264
+ ],
265
+ })
266
+ ]
267
+ });
268
+ ```
269
+
270
+ The portal-plugin gives the BetterAuth Client a set of customer management methods, scoped under `authClient.customer`.
271
+
272
+ ### Customer Portal Management
273
+
274
+ The following method will redirect the user to the Spaire Customer Portal, where they can see orders, purchases, subscriptions, benefits, etc.
275
+
276
+ ```typescript
277
+ await authClient.customer.portal();
278
+ ```
279
+
280
+ ### Customer State
281
+
282
+ The portal plugin also adds a convenient state-method for retrieving the general Customer State.
283
+
284
+ ```typescript
285
+ const { data: customerState } = await authClient.customer.state();
286
+ ```
287
+
288
+ The customer state object contains:
289
+
290
+ - All the data about the customer.
291
+ - The list of their active subscriptions
292
+ - Note: This does not include subscriptions done by a parent organization. See the subscription list-method below for more information.
293
+ - The list of their granted benefits.
294
+ - The list of their active meters, with their current balance.
295
+
296
+ Thus, with that single object, you have all the required information to check if you should provision access to your service or not.
297
+
298
+ [You can learn more about the Spaire Customer State in the Spaire Docs](https://docs.spairehq.com/integrate/customer-state).
299
+
300
+ ### Benefits, Orders & Subscriptions
301
+
302
+ The portal plugin adds 3 convenient methods for listing benefits, orders & subscriptions relevant to the authenticated user/customer.
303
+
304
+ [All of these methods use the Spaire CustomerPortal APIs](https://docs.spairehq.com/api-reference/customer-portal)
305
+
306
+ #### Benefits
307
+
308
+ This method only lists granted benefits for the authenticated user/customer.
309
+
310
+ ```typescript
311
+ const { data: benefits } = await authClient.customer.benefits.list({
312
+ query: {
313
+ page: 1,
314
+ limit: 10,
315
+ },
316
+ });
317
+ ```
318
+
319
+ #### Orders
320
+
321
+ This method lists orders like purchases and subscription renewals for the authenticated user/customer.
322
+
323
+ ```typescript
324
+ const { data: orders } = await authClient.customer.orders.list({
325
+ query: {
326
+ page: 1,
327
+ limit: 10,
328
+ productBillingType: "one_time", // or 'recurring'
329
+ },
330
+ });
331
+ ```
332
+
333
+ #### Subscriptions
334
+
335
+ This method lists the subscriptions associated with authenticated user/customer.
336
+
337
+ ```typescript
338
+ const { data: subscriptions } = await authClient.customer.subscriptions.list({
339
+ query: {
340
+ page: 1,
341
+ limit: 10,
342
+ active: true,
343
+ },
344
+ });
345
+ ```
346
+
347
+ **Important** - Organization Support
348
+
349
+ This will **not** return subscriptions made by a parent organization to the authenticated user.
350
+
351
+ However, you can pass a `referenceId` to this method. This will return all subscriptions associated with that referenceId instead of subscriptions associated with the user.
352
+
353
+ So in order to figure out if a user should have access, pass the user's organization ID to see if there is an active subscription for that organization.
354
+
355
+ ```typescript
356
+ const organizationId = (await authClient.organization.list())?.data?.[0]?.id,
357
+
358
+ const { data: subscriptions } = await authClient.customer.subscriptions.list({
359
+ query: {
360
+ page: 1,
361
+ limit: 10,
362
+ active: true,
363
+ referenceId: organizationId
364
+ },
365
+ });
366
+
367
+ const userShouldHaveAccess = subscriptions.some(
368
+ sub => // Your logic to check subscription product or whatever.
369
+ )
370
+ ```
371
+
372
+ ## Usage Plugin
373
+
374
+ A simple plugin for Usage Based Billing.
375
+
376
+ ```typescript
377
+ import { spaire, checkout, portal, usage } from "@spaire/better-auth";
378
+
379
+ const auth = betterAuth({
380
+ // ... Better Auth config
381
+ plugins: [
382
+ spaire({
383
+ ...
384
+ use: [
385
+ checkout(...),
386
+ portal(),
387
+ usage()
388
+ ],
389
+ })
390
+ ]
391
+ });
392
+ ```
393
+
394
+ ### Event Ingestion
395
+
396
+ Spaire's Usage Based Billing builds entirely on event ingestion. Ingest events from your application, create Meters to represent that usage, and add metered prices to Products to charge for it.
397
+
398
+ [Learn more about Usage Based Billing in the Spaire Docs.](https://docs.spairehq.com/features/usage-based-billing/introduction)
399
+
400
+ ```typescript
401
+ const { data: ingested } = await authClient.usage.ingest({
402
+ event: "file-uploads",
403
+ metadata: {
404
+ uploadedFiles: 12,
405
+ },
406
+ });
407
+ ```
408
+
409
+ The authenticated user is automatically associated with the ingested event.
410
+
411
+ ### Customer Meters
412
+
413
+ A simple method for listing the authenticated user's Usage Meters, or as we call them, Customer Meters.
414
+
415
+ Customer Meter's contains all information about their consumtion on your defined meters.
416
+
417
+ - Customer Information
418
+ - Meter Information
419
+ - Customer Meter Information
420
+ - Consumed Units
421
+ - Credited Units
422
+ - Balance
423
+
424
+ ```typescript
425
+ const { data: customerMeters } = await authClient.usage.meters.list({
426
+ query: {
427
+ page: 1,
428
+ limit: 10,
429
+ },
430
+ });
431
+ ```
432
+
433
+ ## Webhooks Plugin
434
+
435
+ The Webhooks plugin can be used to capture incoming events from your Spaire organization.
436
+
437
+ ```typescript
438
+ import { spaire, webhooks } from "@spaire/better-auth";
439
+
440
+ const auth = betterAuth({
441
+ // ... Better Auth config
442
+ plugins: [
443
+ spaire({
444
+ ...
445
+ use: [
446
+ webhooks({
447
+ secret: process.env.SPAIRE_WEBHOOK_SECRET,
448
+ onCustomerStateChanged: (payload) => // Triggered when anything regarding a customer changes
449
+ onOrderPaid: (payload) => // Triggered when an order was paid (purchase, subscription renewal, etc.)
450
+ ... // Over 25 granular webhook handlers
451
+ onPayload: (payload) => // Catch-all for all events
452
+ })
453
+ ],
454
+ })
455
+ ]
456
+ });
457
+ ```
458
+
459
+ Configure a Webhook endpoint in your Spaire Organization Settings page. Webhook endpoint is configured at /spaire/webhooks.
460
+
461
+ Add the secret to your environment.
462
+
463
+ ```bash
464
+ # .env
465
+ SPAIRE_WEBHOOK_SECRET=...
466
+ ```
467
+
468
+ The plugin supports handlers for all Spaire webhook events:
469
+
470
+ - `onPayload` - Catch-all handler for any incoming Webhook event
471
+ - `onCheckoutCreated` - Triggered when a checkout is created
472
+ - `onCheckoutUpdated` - Triggered when a checkout is updated
473
+ - `onOrderCreated` - Triggered when an order is created
474
+ - `onOrderPaid` - Triggered when an order is paid
475
+ - `onOrderRefunded` - Triggered when an order is refunded
476
+ - `onRefundCreated` - Triggered when a refund is created
477
+ - `onRefundUpdated` - Triggered when a refund is updated
478
+ - `onSubscriptionCreated` - Triggered when a subscription is created
479
+ - `onSubscriptionUpdated` - Triggered when a subscription is updated
480
+ - `onSubscriptionActive` - Triggered when a subscription becomes active
481
+ - `onSubscriptionCanceled` - Triggered when a subscription is canceled
482
+ - `onSubscriptionRevoked` - Triggered when a subscription is revoked
483
+ - `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed
484
+ - `onProductCreated` - Triggered when a product is created
485
+ - `onProductUpdated` - Triggered when a product is updated
486
+ - `onOrganizationUpdated` - Triggered when an organization is updated
487
+ - `onBenefitCreated` - Triggered when a benefit is created
488
+ - `onBenefitUpdated` - Triggered when a benefit is updated
489
+ - `onBenefitGrantCreated` - Triggered when a benefit grant is created
490
+ - `onBenefitGrantUpdated` - Triggered when a benefit grant is updated
491
+ - `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked
492
+ - `onCustomerCreated` - Triggered when a customer is created
493
+ - `onCustomerUpdated` - Triggered when a customer is updated
494
+ - `onCustomerDeleted` - Triggered when a customer is deleted
495
+ - `onCustomerStateChanged` - Triggered when a customer is created
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@spaire/better-auth",
3
+ "version": "2.0.1",
4
+ "description": "Spaire integration for better-auth",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.cts",
16
+ "default": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "./client": {
20
+ "import": {
21
+ "types": "./dist/client.d.ts",
22
+ "default": "./dist/client.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/client.d.cts",
26
+ "default": "./dist/client.cjs"
27
+ }
28
+ }
29
+ },
30
+ "type": "module",
31
+ "engines": {
32
+ "node": ">=16"
33
+ },
34
+ "scripts": {
35
+ "test": "vitest",
36
+ "test:watch": "vitest",
37
+ "test:coverage": "vitest run --coverage",
38
+ "build": "tsup ./src/index.ts ./src/client.ts --format esm,cjs --dts --clean --sourcemap",
39
+ "dev": "npm run build -- --watch",
40
+ "check": "biome check --write ./src"
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "keywords": [
46
+ "spaire",
47
+ "better-auth",
48
+ "payments",
49
+ "subscriptions"
50
+ ],
51
+ "devDependencies": {
52
+ "@biomejs/biome": "1.9.4",
53
+ "@spaire/adapter-utils": "workspace:*",
54
+ "@spaire/sdk": "^0.45.1",
55
+ "@sindresorhus/tsconfig": "^7.0.0",
56
+ "@types/node": "^20.0.0",
57
+ "better-auth": "^1.4.12",
58
+ "tsup": "^8.5.1",
59
+ "vitest": "^2.1.8"
60
+ },
61
+ "peerDependencies": {
62
+ "@spaire/sdk": "^0.45.1",
63
+ "better-auth": "^1.4.12",
64
+ "zod": "^3.24.2 || ^4"
65
+ },
66
+ "dependencies": {
67
+ "@spaire/checkout": "npm:@polar-sh/checkout@^0.1.14"
68
+ }
69
+ }