@raideno/convex-stripe 0.2.0-beta.1 → 0.2.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 +361 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,14 +2,370 @@
|
|
|
2
2
|
|
|
3
3
|
A demo project is available at [https://convex-stripe-demo.vercel.app/](https://convex-stripe-demo.vercel.app/).
|
|
4
4
|
|
|
5
|
-
Stripe subscriptions,
|
|
5
|
+
Stripe [syncing](./references/tables.md), subscriptions, [checkouts](#-checkout-action) and stripe connect for Convex apps. Implemented according to the best practices listed in [Theo's Stripe Recommendations](https://github.com/t3dotgg/stripe-recommendations).
|
|
6
6
|
|
|
7
|
-
## Install
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```sh [npm]
|
|
10
11
|
npm install @raideno/convex-stripe stripe
|
|
11
12
|
```
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
### 1. Set up Stripe
|
|
17
|
+
- Create a Stripe account.
|
|
18
|
+
- Configure a webhook pointing to:
|
|
19
|
+
```
|
|
20
|
+
https://<your-convex-app>.convex.site/stripe/webhook
|
|
21
|
+
```
|
|
22
|
+
- Enable the following [Stripe Events](./references/events.md).
|
|
23
|
+
- Enable the [Stripe Billing Portal](https://dashboard.stripe.com/test/settings/billing/portal).
|
|
24
|
+
|
|
25
|
+
### 2. Set Environment Variables on Convex
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx convex env set STRIPE_SECRET_KEY "<secret>"
|
|
29
|
+
npx convex env set STRIPE_ACCOUNT_WEBHOOK_SECRET "<secret>"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 3. Add tables.
|
|
33
|
+
|
|
34
|
+
Check [Tables Schemas](./references/tables.md) to know more about the synced tables.
|
|
35
|
+
|
|
36
|
+
```ts [convex/schema.ts]
|
|
37
|
+
import { defineSchema } from "convex/server";
|
|
38
|
+
import { stripeTables } from "@raideno/convex-stripe/server";
|
|
39
|
+
|
|
40
|
+
export default defineSchema({
|
|
41
|
+
...stripeTables,
|
|
42
|
+
// your other tables...
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 4. Initialize the library
|
|
47
|
+
|
|
48
|
+
```ts [convex/stripe.ts]
|
|
49
|
+
import { internalConvexStripe } from "@raideno/convex-stripe/server";
|
|
50
|
+
|
|
51
|
+
export const { stripe, store, sync } = internalConvexStripe({
|
|
52
|
+
stripe: {
|
|
53
|
+
secret_key: process.env.STRIPE_SECRET_KEY!,
|
|
54
|
+
account_webhook_secret: process.env.STRIPE_ACCOUNT_WEBHOOK_SECRET!,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const createCustomer = internalAction({
|
|
59
|
+
args: {
|
|
60
|
+
email: v.optional(v.string()),
|
|
61
|
+
entityId: v.string(),
|
|
62
|
+
},
|
|
63
|
+
handler: async (context, args) => {
|
|
64
|
+
return stripe.customers.create(context, {
|
|
65
|
+
email: args.email,
|
|
66
|
+
entityId: args.entityId,
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> **Note:** All exposed actions (store, sync, createEntity) are **internal**. Meaning they can only be called from other convex functions, you can wrap them in public actions when needed.
|
|
74
|
+
> **Important:** `store` must always be exported, as it is used internally.
|
|
75
|
+
|
|
76
|
+
### 5. Register HTTP routes
|
|
77
|
+
|
|
78
|
+
```ts [convex/http.ts]
|
|
79
|
+
import { httpRouter } from "convex/server";
|
|
80
|
+
import { stripe } from "./stripe";
|
|
81
|
+
|
|
82
|
+
const http = httpRouter();
|
|
83
|
+
|
|
84
|
+
// registers POST /stripe/webhook
|
|
85
|
+
// registers GET /stripe/return/*
|
|
86
|
+
stripe.addHttpRoutes(http);
|
|
87
|
+
|
|
88
|
+
export default http;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 6. Stripe customers
|
|
92
|
+
|
|
93
|
+
Ideally you want to create a stripe customer the moment a new entity (user, organization, etc) is created.
|
|
94
|
+
|
|
95
|
+
An `entityId` refers to something you are billing. It can be a user, organization or any other thing. With each entity must be associated a stripe customer and the stripe customer can be created using the [`createEntity` action](#createentity-action).
|
|
96
|
+
|
|
97
|
+
Below are with different auth providers examples where the user is the entity we are billing.
|
|
98
|
+
|
|
99
|
+
::: code-group
|
|
100
|
+
|
|
101
|
+
```ts [convex-auth]
|
|
102
|
+
"convex/auth.ts"
|
|
103
|
+
|
|
104
|
+
// example with convex-auth: https://labs.convex.dev/auth
|
|
105
|
+
|
|
106
|
+
import { convexAuth } from "@convex-dev/auth/server";
|
|
107
|
+
import { Password } from "@convex-dev/auth/providers/Password";
|
|
108
|
+
import { internal } from "./_generated/api";
|
|
109
|
+
|
|
110
|
+
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
|
111
|
+
providers: [Password],
|
|
112
|
+
callbacks: {
|
|
113
|
+
afterUserCreatedOrUpdated: async (context, args) => {
|
|
114
|
+
await context.scheduler.runAfter(0, internal.stripe.createCustomer, {
|
|
115
|
+
/*
|
|
116
|
+
* will call stripe.customers.create
|
|
117
|
+
*/
|
|
118
|
+
entityId: args.userId,
|
|
119
|
+
email: args.profile.email,
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```ts [better-auth]
|
|
127
|
+
"convex/auth.ts"
|
|
128
|
+
|
|
129
|
+
// example with better-auth: https://convex-better-auth.netlify.app/
|
|
130
|
+
|
|
131
|
+
// coming soon...
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```ts [clerk]
|
|
135
|
+
"convex/auth.ts"
|
|
136
|
+
|
|
137
|
+
// example with clerk: https://docs.convex.dev/auth/clerk
|
|
138
|
+
|
|
139
|
+
// coming soon...
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
:::
|
|
143
|
+
|
|
144
|
+
### 7. Run `sync` action
|
|
145
|
+
|
|
146
|
+
In your convex project's dashboard. Go the **Functions** section and execute the `sync` action.
|
|
147
|
+
|
|
148
|
+
This is done to sync already existing stripe data into your convex database.
|
|
149
|
+
It must be done in both your development and production deployments after installing or updating the library.
|
|
150
|
+
|
|
151
|
+
This might not be necessary if you are starting with a fresh empty stripe project.
|
|
152
|
+
|
|
153
|
+
### 8. Start building
|
|
154
|
+
Now you can use the provided functions to:
|
|
155
|
+
- Generate a subscription or payment link [`stripe.subscribe`](#subscribe-function), [`stripe.pay`](#pay-function) for a given entity.
|
|
156
|
+
- Generate a link to the entity's [`stripe.portal`](#portal-function) to manage their subscriptions.
|
|
157
|
+
- Create stripe connect accounts and link them, [`stripe.accounts.create`](#), [`stripe.accounts.link`](#).
|
|
158
|
+
- Consult the [synced tables](./references/tables.md).
|
|
159
|
+
- Etc.
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
## Usage
|
|
163
|
+
|
|
164
|
+
The library automatically syncs the [following tables](./references/tables.md).
|
|
165
|
+
|
|
166
|
+
You can query these tables at any time to:
|
|
167
|
+
|
|
168
|
+
- List available products/plans and prices.
|
|
169
|
+
- Retrieve customers and their `customerId`.
|
|
170
|
+
- Check active subscriptions.
|
|
171
|
+
- Etc.
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
### `customers.create` Function
|
|
175
|
+
|
|
176
|
+
Creates or updates a Stripe customer for a given entity (user or organization). Will call [`stripe.customers.create`](https://docs.stripe.com/api/customers/create) under the hood.
|
|
177
|
+
|
|
178
|
+
This should be called whenever a new entity is created in your app, or when you want to ensure the entity has a Stripe customer associated with it.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { v } from "convex/values";
|
|
182
|
+
import { stripe } from "./stripe";
|
|
183
|
+
import { action, internal } from "./_generated/api";
|
|
184
|
+
|
|
185
|
+
export const createCustomer = internalAction({
|
|
186
|
+
args: {
|
|
187
|
+
email: v.optional(v.string()),
|
|
188
|
+
entityId: v.string(),
|
|
189
|
+
},
|
|
190
|
+
handler: async (context, args) => {
|
|
191
|
+
return stripe.customers.create(context, {
|
|
192
|
+
email: args.email,
|
|
193
|
+
entityId: args.entityId,
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Notes:**
|
|
200
|
+
|
|
201
|
+
- `entityId` is your app’s internal ID (user/org).
|
|
202
|
+
- `customerId` is stripe's internal ID.
|
|
203
|
+
- `email` is optional, but recommended so the Stripe customer has a contact email.
|
|
204
|
+
- If the entity already has a Stripe customer, `createEntity` will return the existing one instead of creating a duplicate.
|
|
205
|
+
- Typically, you’ll call this automatically in your user/org creation flow (see [Configuration - 6](#configuration)).
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
### `sync` Action
|
|
209
|
+
|
|
210
|
+
Synchronizes Stripe resources with your Convex database.
|
|
211
|
+
|
|
212
|
+
This action is typically manually called or setup to be automatically called in your ci/cd pipeline.
|
|
213
|
+
|
|
214
|
+
**Parameters:**
|
|
215
|
+
|
|
216
|
+
- `data` (optional, default: `true`): Syncs all existing Stripe resources to Convex tables.
|
|
217
|
+
- `data.withConnect` (option, default: `false`): Syncs all existing Stripe resources from linked accounts to Convex tables.
|
|
218
|
+
- `webhook.account` (optional, default: `false`): Creates/updates the account webhook endpoint. Returns the webhook secret if a new endpoint is created. You must set it in your convex environment variables as `STRIPE_ACCOUNT_WEBHOOK_SECRET`.
|
|
219
|
+
- `webhook.connect` (optional, default: `false`): Creates/updates the connect webhook endpoint. Returns the webhook secret if a new endpoint is created. You must set it in your convex environment variables as `STRIPE_CONNECT_WEBHOOK_SECRET`.
|
|
220
|
+
- `portal` (optional, default: `false`): Creates the default billing portal configuration if it doesn't exist.
|
|
221
|
+
- `unstable_catalog` (optional, default: `false`): Creates the default provided products and prices passed in the configuration.
|
|
222
|
+
|
|
223
|
+
### `subscribe` Function
|
|
224
|
+
|
|
225
|
+
Creates a Stripe Subscription Checkout session for a given entity. Will call [`stripe.checkout.sessions.create`](https://docs.stripe.com/api/checkout/sessions/create) under the hood, the same parameters can be passed.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { v } from "convex/values";
|
|
229
|
+
|
|
230
|
+
import { stripe } from "./stripe";
|
|
231
|
+
import { action, internal } from "./_generated/api";
|
|
232
|
+
|
|
233
|
+
export const createCheckout = action({
|
|
234
|
+
args: { entityId: v.string(), priceId: v.string() },
|
|
235
|
+
handler: async (context, args) => {
|
|
236
|
+
// TODO: add your own auth/authorization logic here
|
|
237
|
+
|
|
238
|
+
const response = await stripe.subscribe(context, {
|
|
239
|
+
entityId: args.entityId,
|
|
240
|
+
priceId: args.priceId,
|
|
241
|
+
mode: "subscription",
|
|
242
|
+
success_url: "http://localhost:3000/payments/success",
|
|
243
|
+
cancel_url: "http://localhost:3000/payments/cancel",
|
|
244
|
+
/*
|
|
245
|
+
* Other parameters from stripe.checkout.sessions.create(...)
|
|
246
|
+
*/
|
|
247
|
+
}, {
|
|
248
|
+
/*
|
|
249
|
+
* Optional Stripe Request Options
|
|
250
|
+
*/
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return response.url;
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
### `portal` Function
|
|
260
|
+
|
|
261
|
+
Allows an entity to manage their subscription via the Stripe Portal. Will call [`stripe.billingPortal.sessions.create`](https://docs.stripe.com/api/customer_portal/sessions/create) under the hood, the same parameters can be passed.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { v } from "convex/values";
|
|
265
|
+
|
|
266
|
+
import { stripe } from "./stripe";
|
|
267
|
+
import { action, internal } from "./_generated/api";
|
|
268
|
+
|
|
269
|
+
export const portal = action({
|
|
270
|
+
args: { entityId: v.string() },
|
|
271
|
+
handler: async (context, args) => {
|
|
272
|
+
const response = await stripe.portal(context, {
|
|
273
|
+
entityId: args.entityId,
|
|
274
|
+
returnUrl: "http://localhost:3000/return-from-portal",
|
|
275
|
+
/*
|
|
276
|
+
* Other parameters from stripe.billingPortal.sessions.create(...)
|
|
277
|
+
*/
|
|
278
|
+
}, {
|
|
279
|
+
/*
|
|
280
|
+
* Optional Stripe Request Options
|
|
281
|
+
*/
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return response.url;
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
The provided entityId must have a customerId associated to it otherwise the action will throw an error.
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
### `pay` Function
|
|
292
|
+
|
|
293
|
+
Creates a Stripe One Time Payment Checkout session for a given entity. Will call [`stripe.checkout.sessions.create`](https://docs.stripe.com/api/checkout/sessions/create) under the hood, the same parameters can be passed.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import { v } from "convex/values";
|
|
297
|
+
|
|
298
|
+
import { stripe } from "./stripe";
|
|
299
|
+
import { action, internal } from "./_generated/api";
|
|
300
|
+
|
|
301
|
+
export const pay = action({
|
|
302
|
+
args: { entityId: v.string(), orderId: v.string(), priceId: v.string() },
|
|
303
|
+
handler: async (context, args) => {
|
|
304
|
+
// Add your own auth/authorization logic here
|
|
305
|
+
|
|
306
|
+
const response = await stripe.pay(context, {
|
|
307
|
+
referenceId: args.orderId,
|
|
308
|
+
entityId: args.entityId,
|
|
309
|
+
mode: "payment",
|
|
310
|
+
line_items: [{ price: args.priceId, quantity: 1 }],
|
|
311
|
+
success_url: `${process.env.SITE_URL}/?return-from-pay=success`,
|
|
312
|
+
cancel_url: `${process.env.SITE_URL}/?return-from-pay=cancel`,
|
|
313
|
+
/*
|
|
314
|
+
* Other parameters from stripe.checkout.sessions.create(...)
|
|
315
|
+
*/
|
|
316
|
+
}, {
|
|
317
|
+
/*
|
|
318
|
+
* Optional Stripe Request Options
|
|
319
|
+
*/
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return response.url;
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
## Best Practices
|
|
329
|
+
|
|
330
|
+
- Always create a Stripe customer (`createEntity`) when a new entity is created.
|
|
331
|
+
- Use `metadata` or `marketing_features` on products to store feature flags or limits.
|
|
332
|
+
- Run `sync` when you first configure the extension to sync already existing stripe resources.
|
|
333
|
+
- Never expose internal actions directly to clients, wrap them in public actions with proper authorization.
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
## Resources
|
|
337
|
+
|
|
338
|
+
- [Convex Documentation](https://docs.convex.dev)
|
|
339
|
+
- [Stripe Documentation](https://stripe.com/docs)
|
|
340
|
+
- [Demo App](https://convex-stripe-demo.vercel.app/)
|
|
341
|
+
- [GitHub Repository](https://github.com/raideno/convex-stripe)
|
|
342
|
+
- [Theo's Stripe Recommendations](https://github.com/t3dotgg/stripe-recommendations)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
## Development
|
|
346
|
+
|
|
347
|
+
Clone the repository:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
git clone git@github.com:raideno/convex-stripe.git
|
|
351
|
+
cd convex-stripe
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Install the dependencies:
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
npm install
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Start the development server:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
# automatically rebuild lib on changes
|
|
364
|
+
npm run dev --workspace @raideno/convex-stripe
|
|
365
|
+
# run the demo app
|
|
366
|
+
npm run dev --workspace demo
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Contributions
|
|
14
370
|
|
|
15
|
-
|
|
371
|
+
All contributions are welcome! Please open an issue or a PR.
|