kitcn 0.0.1 → 0.12.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/bin/intent.js +3 -0
- package/dist/aggregate/index.d.ts +388 -0
- package/dist/aggregate/index.js +37 -0
- package/dist/api-entry-BckXqaLb.js +66 -0
- package/dist/auth/client/index.d.ts +37 -0
- package/dist/auth/client/index.js +217 -0
- package/dist/auth/config/index.d.ts +45 -0
- package/dist/auth/config/index.js +24 -0
- package/dist/auth/generated/index.d.ts +2 -0
- package/dist/auth/generated/index.js +3 -0
- package/dist/auth/http/index.d.ts +64 -0
- package/dist/auth/http/index.js +461 -0
- package/dist/auth/index.d.ts +221 -0
- package/dist/auth/index.js +1398 -0
- package/dist/auth/nextjs/index.d.ts +50 -0
- package/dist/auth/nextjs/index.js +81 -0
- package/dist/auth-store-Cljlmdmi.js +197 -0
- package/dist/builder-CBdG5W6A.js +1974 -0
- package/dist/caller-factory-cTXNvYdz.js +216 -0
- package/dist/cli.mjs +13255 -0
- package/dist/codegen-lF80HSWu.mjs +3416 -0
- package/dist/context-utils-HPC5nXzx.d.ts +17 -0
- package/dist/create-schema-odyF4kCy.js +156 -0
- package/dist/create-schema-orm-DOyiNDCx.js +246 -0
- package/dist/crpc/index.d.ts +105 -0
- package/dist/crpc/index.js +169 -0
- package/dist/customFunctions-C0voKmtx.js +144 -0
- package/dist/error-BZEnI7Sq.js +41 -0
- package/dist/generated-contract-disabled-Cih4eITO.js +50 -0
- package/dist/generated-contract-disabled-D-sOFy92.d.ts +354 -0
- package/dist/http-types-DqJubRPJ.d.ts +292 -0
- package/dist/meta-utils-0Pu0Nrap.js +117 -0
- package/dist/middleware-BUybuv9n.d.ts +34 -0
- package/dist/middleware-C2qTZ3V7.js +84 -0
- package/dist/orm/index.d.ts +17 -0
- package/dist/orm/index.js +10713 -0
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +3 -0
- package/dist/procedure-caller-DtxLmGwA.d.ts +1467 -0
- package/dist/procedure-caller-MWcxhQDv.js +349 -0
- package/dist/query-context-B8o6-8kC.js +1518 -0
- package/dist/query-context-CFZqIvD7.d.ts +42 -0
- package/dist/query-options-Dw7cOyXl.js +121 -0
- package/dist/ratelimit/index.d.ts +269 -0
- package/dist/ratelimit/index.js +856 -0
- package/dist/ratelimit/react/index.d.ts +76 -0
- package/dist/ratelimit/react/index.js +183 -0
- package/dist/react/index.d.ts +1284 -0
- package/dist/react/index.js +2526 -0
- package/dist/rsc/index.d.ts +276 -0
- package/dist/rsc/index.js +233 -0
- package/dist/runtime-CtvJPkur.js +2453 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +6 -0
- package/dist/solid/index.d.ts +1221 -0
- package/dist/solid/index.js +2940 -0
- package/dist/transformer-DtDhR3Lc.js +194 -0
- package/dist/types-BTb_4BaU.d.ts +42 -0
- package/dist/types-BiJE7qxR.d.ts +4 -0
- package/dist/types-DEJpkIhw.d.ts +88 -0
- package/dist/types-HhO_R6pd.d.ts +213 -0
- package/dist/validators-B7oIJCAp.js +279 -0
- package/dist/validators-vzRKjBJC.d.ts +88 -0
- package/dist/watcher.mjs +96 -0
- package/dist/where-clause-compiler-DdjN63Io.d.ts +4756 -0
- package/package.json +107 -35
- package/skills/convex/SKILL.md +486 -0
- package/skills/convex/references/features/aggregates.md +353 -0
- package/skills/convex/references/features/auth-admin.md +446 -0
- package/skills/convex/references/features/auth-organizations.md +1141 -0
- package/skills/convex/references/features/auth-polar.md +579 -0
- package/skills/convex/references/features/auth.md +470 -0
- package/skills/convex/references/features/create-plugins.md +153 -0
- package/skills/convex/references/features/http.md +676 -0
- package/skills/convex/references/features/migrations.md +162 -0
- package/skills/convex/references/features/orm.md +1166 -0
- package/skills/convex/references/features/react.md +657 -0
- package/skills/convex/references/features/scheduling.md +267 -0
- package/skills/convex/references/features/testing.md +209 -0
- package/skills/convex/references/setup/auth.md +501 -0
- package/skills/convex/references/setup/biome.md +190 -0
- package/skills/convex/references/setup/doc-guidelines.md +145 -0
- package/skills/convex/references/setup/index.md +759 -0
- package/skills/convex/references/setup/next.md +116 -0
- package/skills/convex/references/setup/react.md +175 -0
- package/skills/convex/references/setup/server.md +473 -0
- package/skills/convex/references/setup/start.md +67 -0
- package/LICENSE +0 -21
- package/README.md +0 -0
- package/dist/index.d.mts +0 -5
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -6
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
# Auth Polar Plugin
|
|
2
|
+
|
|
3
|
+
Polar payment/subscription integration via Better Auth plugin. Webhook-driven subscription truth, `ctx.orm` for billing state, feature gating by active subscription.
|
|
4
|
+
|
|
5
|
+
Prerequisites: `setup/auth.md`, `setup/server.md`.
|
|
6
|
+
|
|
7
|
+
See [Better Auth Polar Plugin](https://www.better-auth.com/docs/plugins/polar) for full API reference.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add @polar-sh/better-auth @polar-sh/sdk buffer
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Server Config
|
|
16
|
+
|
|
17
|
+
### Polyfills (Conditional)
|
|
18
|
+
|
|
19
|
+
Convex needs Buffer polyfill for Polar SDK:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// convex/lib/polar-polyfills.ts
|
|
23
|
+
import { Buffer as BufferPolyfill } from 'buffer';
|
|
24
|
+
globalThis.Buffer = BufferPolyfill;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Polar Client
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// convex/lib/polar-client.ts
|
|
31
|
+
import { Polar } from '@polar-sh/sdk';
|
|
32
|
+
|
|
33
|
+
export const getPolarClient = () =>
|
|
34
|
+
new Polar({
|
|
35
|
+
accessToken: process.env.POLAR_ACCESS_TOKEN!,
|
|
36
|
+
server: process.env.POLAR_SERVER === 'production' ? 'production' : 'sandbox',
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Better Auth with Polar Plugin
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// convex/functions/auth.ts
|
|
44
|
+
// IMPORTANT: Import polyfills FIRST
|
|
45
|
+
import '../lib/polar-polyfills';
|
|
46
|
+
|
|
47
|
+
import { checkout, polar, portal, usage, webhooks } from '@polar-sh/better-auth';
|
|
48
|
+
import { Polar } from '@polar-sh/sdk';
|
|
49
|
+
import { createPolarCustomerCaller } from './generated/polarCustomer.runtime';
|
|
50
|
+
import { createPolarSubscriptionCaller } from './generated/polarSubscription.runtime';
|
|
51
|
+
import { defineAuth } from './generated/auth';
|
|
52
|
+
|
|
53
|
+
export default defineAuth((ctx) => ({
|
|
54
|
+
// ... existing config
|
|
55
|
+
plugins: [
|
|
56
|
+
polar({
|
|
57
|
+
client: new Polar({
|
|
58
|
+
accessToken: process.env.POLAR_ACCESS_TOKEN!,
|
|
59
|
+
server: process.env.POLAR_SERVER === 'production' ? 'production' : 'sandbox',
|
|
60
|
+
}),
|
|
61
|
+
// createCustomerOnSignUp: true, // Use trigger instead (recommended for Convex)
|
|
62
|
+
use: [
|
|
63
|
+
checkout({
|
|
64
|
+
authenticatedUsersOnly: true,
|
|
65
|
+
products: [
|
|
66
|
+
{ productId: process.env.POLAR_PRODUCT_PREMIUM!, slug: 'premium' },
|
|
67
|
+
],
|
|
68
|
+
successUrl: `${process.env.SITE_URL}/success?checkout_id={CHECKOUT_ID}`,
|
|
69
|
+
theme: 'light',
|
|
70
|
+
}),
|
|
71
|
+
portal(),
|
|
72
|
+
usage(),
|
|
73
|
+
webhooks({
|
|
74
|
+
secret: process.env.POLAR_WEBHOOK_SECRET!,
|
|
75
|
+
onCustomerCreated: async (payload) => {
|
|
76
|
+
const userId = payload?.data.externalId;
|
|
77
|
+
if (!userId) return;
|
|
78
|
+
const caller = createPolarCustomerCaller(ctx);
|
|
79
|
+
await caller.updateUserPolarCustomerId({
|
|
80
|
+
customerId: payload.data.id, userId,
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
onSubscriptionCreated: async (payload) => {
|
|
84
|
+
if (!payload.data.customer.externalId) return;
|
|
85
|
+
const caller = createPolarSubscriptionCaller(ctx);
|
|
86
|
+
await caller.createSubscription({
|
|
87
|
+
subscription: convertToDatabaseSubscription(payload.data),
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
onSubscriptionUpdated: async (payload) => {
|
|
91
|
+
if (!payload.data.customer.externalId) return;
|
|
92
|
+
const caller = createPolarSubscriptionCaller(ctx);
|
|
93
|
+
await caller.updateSubscription({
|
|
94
|
+
subscription: convertToDatabaseSubscription(payload.data),
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
],
|
|
99
|
+
}),
|
|
100
|
+
],
|
|
101
|
+
}));
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Customer Creation via Trigger
|
|
105
|
+
|
|
106
|
+
Create Polar customer asynchronously on signup:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// convex/functions/auth.ts
|
|
110
|
+
import { defineAuth } from './generated/auth';
|
|
111
|
+
|
|
112
|
+
export default defineAuth((ctx) => ({
|
|
113
|
+
triggers: {
|
|
114
|
+
user: {
|
|
115
|
+
create: {
|
|
116
|
+
after: async (user, triggerCtx) => {
|
|
117
|
+
const caller = createPolarCustomerCaller(ctx);
|
|
118
|
+
await caller.schedule.now.createCustomer({
|
|
119
|
+
email: user.email,
|
|
120
|
+
name: user.name || user.username,
|
|
121
|
+
userId: user.id,
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}));
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Customer Deletion Sync
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// convex/functions/auth.ts
|
|
134
|
+
import { defineAuth } from './generated/auth';
|
|
135
|
+
|
|
136
|
+
export default defineAuth((ctx) => ({
|
|
137
|
+
user: {
|
|
138
|
+
deleteUser: {
|
|
139
|
+
enabled: true,
|
|
140
|
+
afterDelete: async (user) => {
|
|
141
|
+
const polar = getPolarClient();
|
|
142
|
+
await polar.customers.deleteExternal({ externalId: user.id });
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}));
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Client Config
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// src/lib/convex/auth-client.ts
|
|
153
|
+
import { polarClient } from '@polar-sh/better-auth';
|
|
154
|
+
|
|
155
|
+
export const authClient = createAuthClient({
|
|
156
|
+
plugins: [polarClient()],
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Schema
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// convex/functions/schema.ts
|
|
164
|
+
import { boolean, convexTable, defineSchema, id, index, integer, json, text } from 'kitcn/orm';
|
|
165
|
+
|
|
166
|
+
// User table — add Polar customer ID
|
|
167
|
+
export const user = convexTable('user', {
|
|
168
|
+
// ... existing fields
|
|
169
|
+
customerId: text(), // Polar customer ID
|
|
170
|
+
}, (t) => [index('customerId').on(t.customerId)]);
|
|
171
|
+
|
|
172
|
+
// Subscriptions table — organization-based
|
|
173
|
+
export const subscriptions = convexTable('subscriptions', {
|
|
174
|
+
subscriptionId: text().notNull(),
|
|
175
|
+
organizationId: text().notNull(),
|
|
176
|
+
userId: id('user').notNull(),
|
|
177
|
+
productId: text().notNull(),
|
|
178
|
+
priceId: text(),
|
|
179
|
+
status: text().notNull(), // 'active', 'canceled', 'trialing', 'past_due'
|
|
180
|
+
amount: integer(),
|
|
181
|
+
currency: text(),
|
|
182
|
+
recurringInterval: text(),
|
|
183
|
+
currentPeriodStart: text().notNull(),
|
|
184
|
+
currentPeriodEnd: text(),
|
|
185
|
+
cancelAtPeriodEnd: boolean().notNull(),
|
|
186
|
+
startedAt: text(),
|
|
187
|
+
endedAt: text(),
|
|
188
|
+
createdAt: text().notNull(),
|
|
189
|
+
modifiedAt: text(),
|
|
190
|
+
checkoutId: text(),
|
|
191
|
+
metadata: json<Record<string, unknown>>(),
|
|
192
|
+
customerCancellationReason: text(),
|
|
193
|
+
customerCancellationComment: text(),
|
|
194
|
+
}, (t) => [
|
|
195
|
+
index('subscriptionId').on(t.subscriptionId),
|
|
196
|
+
index('organizationId').on(t.organizationId),
|
|
197
|
+
index('organizationId_status').on(t.organizationId, t.status),
|
|
198
|
+
index('userId_status').on(t.userId, t.status),
|
|
199
|
+
]);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Subscription Conversion Helper
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// convex/lib/polar-helpers.ts
|
|
206
|
+
import type { Subscription } from '@polar-sh/sdk/models/components/subscription';
|
|
207
|
+
import type { WithoutSystemFields } from 'convex/server';
|
|
208
|
+
import type { Doc, Id } from '../functions/_generated/dataModel';
|
|
209
|
+
|
|
210
|
+
export const convertToDatabaseSubscription = (
|
|
211
|
+
subscription: Subscription
|
|
212
|
+
): WithoutSystemFields<Doc<'subscriptions'>> => {
|
|
213
|
+
const organizationId = subscription.metadata?.referenceId as Id<'organization'>;
|
|
214
|
+
if (!organizationId) {
|
|
215
|
+
throw new Error('Subscription missing organizationId in metadata.referenceId');
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
amount: subscription.amount,
|
|
219
|
+
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
|
|
220
|
+
checkoutId: subscription.checkoutId,
|
|
221
|
+
createdAt: subscription.createdAt.toISOString(),
|
|
222
|
+
currency: subscription.currency,
|
|
223
|
+
currentPeriodEnd: subscription.currentPeriodEnd?.toISOString() ?? null,
|
|
224
|
+
currentPeriodStart: subscription.currentPeriodStart.toISOString(),
|
|
225
|
+
customerCancellationComment: subscription.customerCancellationComment,
|
|
226
|
+
customerCancellationReason: subscription.customerCancellationReason,
|
|
227
|
+
endedAt: subscription.endedAt?.toISOString() ?? null,
|
|
228
|
+
metadata: subscription.metadata ?? {},
|
|
229
|
+
modifiedAt: subscription.modifiedAt?.toISOString() ?? null,
|
|
230
|
+
organizationId,
|
|
231
|
+
productId: subscription.productId,
|
|
232
|
+
recurringInterval: subscription.recurringInterval,
|
|
233
|
+
startedAt: subscription.startedAt?.toISOString() ?? null,
|
|
234
|
+
status: subscription.status,
|
|
235
|
+
subscriptionId: subscription.id,
|
|
236
|
+
// IMPORTANT: Use externalId, not metadata.userId
|
|
237
|
+
userId: subscription.customer.externalId as Id<'user'>,
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Shared Product Catalog (Example-Parity Optional)
|
|
243
|
+
|
|
244
|
+
If UI and backend both need plan metadata, keep a shared module:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
// convex/shared/polar-shared.ts
|
|
248
|
+
export const SubscriptionPlan = {
|
|
249
|
+
Free: 'free',
|
|
250
|
+
Premium: 'premium',
|
|
251
|
+
} as const;
|
|
252
|
+
|
|
253
|
+
export type SubscriptionPlan =
|
|
254
|
+
(typeof SubscriptionPlan)[keyof typeof SubscriptionPlan];
|
|
255
|
+
|
|
256
|
+
export const PLANS = {
|
|
257
|
+
[SubscriptionPlan.Free]: { key: SubscriptionPlan.Free, price: 0, credits: 0 },
|
|
258
|
+
[SubscriptionPlan.Premium]: {
|
|
259
|
+
key: SubscriptionPlan.Premium,
|
|
260
|
+
price: 20,
|
|
261
|
+
credits: 2000,
|
|
262
|
+
productId: process.env.POLAR_PRODUCT_PREMIUM ?? 'premium',
|
|
263
|
+
},
|
|
264
|
+
} as const;
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Use this for UI pricing cards and server-side entitlement mapping.
|
|
268
|
+
|
|
269
|
+
## Checkout Plugin
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
checkout({
|
|
273
|
+
products: [
|
|
274
|
+
{ productId: 'uuid-from-polar', slug: 'pro' },
|
|
275
|
+
{ productId: 'uuid-from-polar', slug: 'enterprise' },
|
|
276
|
+
],
|
|
277
|
+
successUrl: `${process.env.SITE_URL}/success?checkout_id={CHECKOUT_ID}`,
|
|
278
|
+
returnUrl: `${process.env.SITE_URL}`, // Optional back button
|
|
279
|
+
authenticatedUsersOnly: true,
|
|
280
|
+
theme: 'light', // or 'dark'
|
|
281
|
+
}),
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Client Checkout
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
// Using slug
|
|
288
|
+
await authClient.checkout({ slug: 'pro', referenceId: organizationId });
|
|
289
|
+
|
|
290
|
+
// Using product ID
|
|
291
|
+
await authClient.checkout({
|
|
292
|
+
products: ['e651f46d-ac20-4f26-b769-ad088b123df2'],
|
|
293
|
+
referenceId: organizationId,
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Organization-Based Checkout
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
const handleSubscribe = async () => {
|
|
301
|
+
const activeOrganizationId = user.activeOrganization?.id;
|
|
302
|
+
if (!activeOrganizationId) { toast.error('Please select an organization'); return; }
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
if (currentUser.plan) {
|
|
306
|
+
await authClient.customer.portal(); // Manage existing
|
|
307
|
+
} else {
|
|
308
|
+
await authClient.checkout({ slug: 'premium', referenceId: activeOrganizationId });
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error('Polar checkout error:', error);
|
|
312
|
+
toast.error('Failed to open checkout');
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Portal Plugin
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
await authClient.customer.portal(); // Open self-service portal
|
|
321
|
+
const { data } = await authClient.customer.state(); // Customer data + subscriptions + benefits + meters
|
|
322
|
+
|
|
323
|
+
// List APIs
|
|
324
|
+
const { data: benefits } = await authClient.customer.benefits.list({ query: { page: 1, limit: 10 } });
|
|
325
|
+
const { data: orders } = await authClient.customer.orders.list({
|
|
326
|
+
query: { page: 1, limit: 10, productBillingType: 'one_time' }, // or 'recurring'
|
|
327
|
+
});
|
|
328
|
+
const { data: subs } = await authClient.customer.subscriptions.list({
|
|
329
|
+
query: { page: 1, limit: 10, active: true },
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Organization subscriptions
|
|
333
|
+
const orgId = (await authClient.organization.list())?.data?.[0]?.id;
|
|
334
|
+
const { data: orgSubs } = await authClient.customer.orders.list({
|
|
335
|
+
query: { page: 1, limit: 10, active: true, referenceId: orgId },
|
|
336
|
+
});
|
|
337
|
+
const userShouldHaveAccess = orgSubs.some(
|
|
338
|
+
(sub) => sub.productId === process.env.NEXT_PUBLIC_POLAR_PRODUCT_PREMIUM
|
|
339
|
+
);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Usage Plugin
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
// Event ingestion
|
|
346
|
+
const { data } = await authClient.usage.ingestion({
|
|
347
|
+
event: 'file-uploads',
|
|
348
|
+
metadata: { uploadedFiles: 12, totalSizeBytes: 1024000 },
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Customer meters (consumed units, credited units, balance)
|
|
352
|
+
const { data: meters } = await authClient.usage.meters.list({ query: { page: 1, limit: 10 } });
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Webhooks Plugin
|
|
356
|
+
|
|
357
|
+
All available handlers:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
webhooks({
|
|
361
|
+
secret: process.env.POLAR_WEBHOOK_SECRET!,
|
|
362
|
+
// Checkout
|
|
363
|
+
onCheckoutCreated, onCheckoutUpdated,
|
|
364
|
+
// Orders
|
|
365
|
+
onOrderCreated, onOrderPaid, onOrderRefunded,
|
|
366
|
+
// Refunds
|
|
367
|
+
onRefundCreated, onRefundUpdated,
|
|
368
|
+
// Subscriptions
|
|
369
|
+
onSubscriptionCreated, onSubscriptionUpdated, onSubscriptionActive,
|
|
370
|
+
onSubscriptionCanceled, onSubscriptionRevoked, onSubscriptionUncanceled,
|
|
371
|
+
// Products
|
|
372
|
+
onProductCreated, onProductUpdated,
|
|
373
|
+
// Benefits
|
|
374
|
+
onBenefitCreated, onBenefitUpdated,
|
|
375
|
+
onBenefitGrantCreated, onBenefitGrantUpdated, onBenefitGrantRevoked,
|
|
376
|
+
// Customers
|
|
377
|
+
onCustomerCreated, onCustomerUpdated, onCustomerDeleted, onCustomerStateChanged,
|
|
378
|
+
// Catch-all
|
|
379
|
+
onPayload,
|
|
380
|
+
}),
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Convex Functions
|
|
384
|
+
|
|
385
|
+
### Customer Management
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
// convex/functions/polarCustomer.ts
|
|
389
|
+
import '../lib/polar-polyfills';
|
|
390
|
+
import { CRPCError } from 'kitcn/server';
|
|
391
|
+
import { z } from 'zod';
|
|
392
|
+
import { privateAction, privateMutation } from '../lib/crpc';
|
|
393
|
+
import { getPolarClient } from '../lib/polar-client';
|
|
394
|
+
|
|
395
|
+
// Create Polar customer (called from user.onCreate trigger)
|
|
396
|
+
export const createCustomer = privateAction
|
|
397
|
+
.input(z.object({ email: z.string().email(), name: z.string().optional(), userId: z.string() }))
|
|
398
|
+
|
|
399
|
+
.action(async ({ input: args }) => {
|
|
400
|
+
const polar = getPolarClient();
|
|
401
|
+
try {
|
|
402
|
+
await polar.customers.create({
|
|
403
|
+
email: args.email,
|
|
404
|
+
externalId: args.userId, // Links Polar customer to Convex user
|
|
405
|
+
name: args.name,
|
|
406
|
+
});
|
|
407
|
+
} catch (error) {
|
|
408
|
+
console.error('Failed to create Polar customer:', error);
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Link Polar customer ID to user (called from webhook)
|
|
414
|
+
export const updateUserPolarCustomerId = privateMutation
|
|
415
|
+
.input(z.object({ customerId: z.string(), userId: z.string() }))
|
|
416
|
+
|
|
417
|
+
.mutation(async ({ ctx, input: args }) => {
|
|
418
|
+
const targetUser = await ctx.orm.query.user.findFirst({ where: { id: args.userId } });
|
|
419
|
+
if (!targetUser) throw new CRPCError({ code: 'NOT_FOUND', message: 'User not found' });
|
|
420
|
+
|
|
421
|
+
const existingUser = await ctx.orm.query.user.findFirst({ where: { customerId: args.customerId } });
|
|
422
|
+
if (existingUser && existingUser.id !== args.userId) {
|
|
423
|
+
throw new CRPCError({ code: 'CONFLICT', message: `Another user already has Polar customer ID ${args.customerId}` });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
await ctx.orm.update(user).set({ customerId: args.customerId }).where(eq(user.id, targetUser.id));
|
|
427
|
+
return null;
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Subscription Management
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
// convex/functions/polarSubscription.ts
|
|
435
|
+
import '../lib/polar-polyfills';
|
|
436
|
+
import { CRPCError } from 'kitcn/server';
|
|
437
|
+
import { z } from 'zod';
|
|
438
|
+
import { authAction, privateMutation, privateQuery } from '../lib/crpc';
|
|
439
|
+
import { getPolarClient } from '../lib/polar-client';
|
|
440
|
+
import { createPolarSubscriptionCaller } from './generated/polarSubscription.runtime';
|
|
441
|
+
|
|
442
|
+
// Create subscription (called from webhook)
|
|
443
|
+
export const createSubscription = privateMutation
|
|
444
|
+
.input(z.object({ subscription: subscriptionSchema }))
|
|
445
|
+
|
|
446
|
+
.mutation(async ({ ctx, input: args }) => {
|
|
447
|
+
const existing = await ctx.orm.query.subscriptions.findFirst({
|
|
448
|
+
where: { subscriptionId: args.subscription.subscriptionId },
|
|
449
|
+
});
|
|
450
|
+
if (existing) {
|
|
451
|
+
throw new CRPCError({ code: 'CONFLICT', message: `Subscription ${args.subscription.subscriptionId} already exists` });
|
|
452
|
+
}
|
|
453
|
+
await ctx.orm.insert(subscriptions).values(args.subscription);
|
|
454
|
+
return null;
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Update subscription (called from webhook)
|
|
458
|
+
export const updateSubscription = privateMutation
|
|
459
|
+
.input(z.object({ subscription: subscriptionSchema }))
|
|
460
|
+
.output(z.object({ updated: z.boolean() }))
|
|
461
|
+
.mutation(async ({ ctx, input: args }) => {
|
|
462
|
+
const existing = await ctx.orm.query.subscriptions.findFirst({
|
|
463
|
+
where: { subscriptionId: args.subscription.subscriptionId },
|
|
464
|
+
});
|
|
465
|
+
if (!existing) return { updated: false };
|
|
466
|
+
await ctx.orm.update(subscriptions).set(args.subscription).where(eq(subscriptions.id, existing.id));
|
|
467
|
+
return { updated: true };
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Get active subscription for user
|
|
471
|
+
export const getActiveSubscription = privateQuery
|
|
472
|
+
.input(z.object({ userId: z.string() }))
|
|
473
|
+
.output(z.object({ subscriptionId: z.string() }).nullable())
|
|
474
|
+
.query(async ({ ctx, input: args }) => {
|
|
475
|
+
const subscription = await ctx.orm.query.subscriptions.findFirst({
|
|
476
|
+
where: { userId: args.userId, status: 'active' },
|
|
477
|
+
orderBy: { createdAt: 'desc' },
|
|
478
|
+
});
|
|
479
|
+
if (!subscription) return null;
|
|
480
|
+
return { subscriptionId: subscription.subscriptionId };
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Cancel subscription (user action)
|
|
484
|
+
export const cancelSubscription = authAction
|
|
485
|
+
.output(z.object({ success: z.boolean() }))
|
|
486
|
+
.action(async ({ ctx }) => {
|
|
487
|
+
const polar = getPolarClient();
|
|
488
|
+
|
|
489
|
+
const caller = createPolarSubscriptionCaller(ctx);
|
|
490
|
+
const subscription = await caller.getActiveSubscription({ userId: ctx.userId! });
|
|
491
|
+
|
|
492
|
+
if (!subscription) {
|
|
493
|
+
throw new CRPCError({ code: 'PRECONDITION_FAILED', message: 'No active subscription found' });
|
|
494
|
+
}
|
|
495
|
+
await polar.subscriptions.update({
|
|
496
|
+
id: subscription.subscriptionId,
|
|
497
|
+
subscriptionUpdate: { cancelAtPeriodEnd: true },
|
|
498
|
+
});
|
|
499
|
+
return { success: true };
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Resume subscription (user action)
|
|
503
|
+
export const resumeSubscription = authAction
|
|
504
|
+
.output(z.object({ success: z.boolean() }))
|
|
505
|
+
.action(async ({ ctx }) => {
|
|
506
|
+
const polar = getPolarClient();
|
|
507
|
+
|
|
508
|
+
const caller = createPolarSubscriptionCaller(ctx);
|
|
509
|
+
const subscription = await caller.getActiveSubscription({ userId: ctx.userId! });
|
|
510
|
+
|
|
511
|
+
if (!subscription) {
|
|
512
|
+
throw new CRPCError({ code: 'PRECONDITION_FAILED', message: 'No active subscription found' });
|
|
513
|
+
}
|
|
514
|
+
await polar.subscriptions.update({
|
|
515
|
+
id: subscription.subscriptionId,
|
|
516
|
+
subscriptionUpdate: { cancelAtPeriodEnd: false },
|
|
517
|
+
});
|
|
518
|
+
return { success: true };
|
|
519
|
+
});
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
## Environment Variables
|
|
523
|
+
|
|
524
|
+
```bash
|
|
525
|
+
# convex/.env
|
|
526
|
+
POLAR_SERVER="sandbox" # 'production' | 'sandbox'
|
|
527
|
+
POLAR_ACCESS_TOKEN="polar_at_..." # Organization access token
|
|
528
|
+
POLAR_WEBHOOK_SECRET="whsec_..." # Webhook signature secret
|
|
529
|
+
POLAR_PRODUCT_PREMIUM="uuid-here" # Premium subscription product
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
## Local Development with Ngrok
|
|
533
|
+
|
|
534
|
+
Polar webhooks require a public URL.
|
|
535
|
+
|
|
536
|
+
1. Install ngrok, reserve a free static domain in [ngrok dashboard](https://dashboard.ngrok.com/domains)
|
|
537
|
+
2. Add to `package.json`:
|
|
538
|
+
```json
|
|
539
|
+
{ "scripts": { "dev": "concurrently 'next dev' 'bun ngrok'", "ngrok": "ngrok http --url=your-domain.ngrok-free.app 3000" } }
|
|
540
|
+
```
|
|
541
|
+
3. Configure webhook URL in Polar Dashboard: `https://your-domain.ngrok-free.app/api/auth/polar/webhooks`
|
|
542
|
+
|
|
543
|
+
## Common Patterns
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
// Check organization subscription
|
|
547
|
+
const subscription = await ctx.orm.query.subscriptions.findFirst({
|
|
548
|
+
where: { organizationId, status: 'active' },
|
|
549
|
+
});
|
|
550
|
+
const isActive = subscription?.status === 'active';
|
|
551
|
+
|
|
552
|
+
// Check user subscription
|
|
553
|
+
const subscription = await ctx.orm.query.subscriptions.findFirst({
|
|
554
|
+
where: { userId, status: 'active' },
|
|
555
|
+
});
|
|
556
|
+
const isPremium = !!subscription;
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Example-parity helper module:
|
|
560
|
+
- `convex/lib/auth/premium-guard.ts` for a reusable `PAYMENT_REQUIRED` guard on premium-only procedures.
|
|
561
|
+
|
|
562
|
+
## API Reference
|
|
563
|
+
|
|
564
|
+
| Operation | Method | Type |
|
|
565
|
+
|-----------|--------|------|
|
|
566
|
+
| Checkout | `authClient.checkout` | Client |
|
|
567
|
+
| Customer portal | `authClient.customer.portal` | Client |
|
|
568
|
+
| Customer state | `authClient.customer.state` | Client |
|
|
569
|
+
| List benefits | `authClient.customer.benefits.list` | Client |
|
|
570
|
+
| List orders | `authClient.customer.orders.list` | Client |
|
|
571
|
+
| List subscriptions | `authClient.customer.subscriptions.list` | Client |
|
|
572
|
+
| Event ingestion | `authClient.usage.ingestion` | Client |
|
|
573
|
+
| List meters | `authClient.usage.meters.list` | Client |
|
|
574
|
+
| Create customer | `internal.polarCustomer.createCustomer` | Internal action |
|
|
575
|
+
| Link customer ID | `internal.polarCustomer.updateUserPolarCustomerId` | Internal mutation |
|
|
576
|
+
| Create subscription | `internal.polarSubscription.createSubscription` | Internal mutation |
|
|
577
|
+
| Update subscription | `internal.polarSubscription.updateSubscription` | Internal mutation |
|
|
578
|
+
| Cancel subscription | Convex action | User action |
|
|
579
|
+
| Resume subscription | Convex action | User action |
|