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.
Files changed (93) hide show
  1. package/bin/intent.js +3 -0
  2. package/dist/aggregate/index.d.ts +388 -0
  3. package/dist/aggregate/index.js +37 -0
  4. package/dist/api-entry-BckXqaLb.js +66 -0
  5. package/dist/auth/client/index.d.ts +37 -0
  6. package/dist/auth/client/index.js +217 -0
  7. package/dist/auth/config/index.d.ts +45 -0
  8. package/dist/auth/config/index.js +24 -0
  9. package/dist/auth/generated/index.d.ts +2 -0
  10. package/dist/auth/generated/index.js +3 -0
  11. package/dist/auth/http/index.d.ts +64 -0
  12. package/dist/auth/http/index.js +461 -0
  13. package/dist/auth/index.d.ts +221 -0
  14. package/dist/auth/index.js +1398 -0
  15. package/dist/auth/nextjs/index.d.ts +50 -0
  16. package/dist/auth/nextjs/index.js +81 -0
  17. package/dist/auth-store-Cljlmdmi.js +197 -0
  18. package/dist/builder-CBdG5W6A.js +1974 -0
  19. package/dist/caller-factory-cTXNvYdz.js +216 -0
  20. package/dist/cli.mjs +13255 -0
  21. package/dist/codegen-lF80HSWu.mjs +3416 -0
  22. package/dist/context-utils-HPC5nXzx.d.ts +17 -0
  23. package/dist/create-schema-odyF4kCy.js +156 -0
  24. package/dist/create-schema-orm-DOyiNDCx.js +246 -0
  25. package/dist/crpc/index.d.ts +105 -0
  26. package/dist/crpc/index.js +169 -0
  27. package/dist/customFunctions-C0voKmtx.js +144 -0
  28. package/dist/error-BZEnI7Sq.js +41 -0
  29. package/dist/generated-contract-disabled-Cih4eITO.js +50 -0
  30. package/dist/generated-contract-disabled-D-sOFy92.d.ts +354 -0
  31. package/dist/http-types-DqJubRPJ.d.ts +292 -0
  32. package/dist/meta-utils-0Pu0Nrap.js +117 -0
  33. package/dist/middleware-BUybuv9n.d.ts +34 -0
  34. package/dist/middleware-C2qTZ3V7.js +84 -0
  35. package/dist/orm/index.d.ts +17 -0
  36. package/dist/orm/index.js +10713 -0
  37. package/dist/plugins/index.d.ts +2 -0
  38. package/dist/plugins/index.js +3 -0
  39. package/dist/procedure-caller-DtxLmGwA.d.ts +1467 -0
  40. package/dist/procedure-caller-MWcxhQDv.js +349 -0
  41. package/dist/query-context-B8o6-8kC.js +1518 -0
  42. package/dist/query-context-CFZqIvD7.d.ts +42 -0
  43. package/dist/query-options-Dw7cOyXl.js +121 -0
  44. package/dist/ratelimit/index.d.ts +269 -0
  45. package/dist/ratelimit/index.js +856 -0
  46. package/dist/ratelimit/react/index.d.ts +76 -0
  47. package/dist/ratelimit/react/index.js +183 -0
  48. package/dist/react/index.d.ts +1284 -0
  49. package/dist/react/index.js +2526 -0
  50. package/dist/rsc/index.d.ts +276 -0
  51. package/dist/rsc/index.js +233 -0
  52. package/dist/runtime-CtvJPkur.js +2453 -0
  53. package/dist/server/index.d.ts +5 -0
  54. package/dist/server/index.js +6 -0
  55. package/dist/solid/index.d.ts +1221 -0
  56. package/dist/solid/index.js +2940 -0
  57. package/dist/transformer-DtDhR3Lc.js +194 -0
  58. package/dist/types-BTb_4BaU.d.ts +42 -0
  59. package/dist/types-BiJE7qxR.d.ts +4 -0
  60. package/dist/types-DEJpkIhw.d.ts +88 -0
  61. package/dist/types-HhO_R6pd.d.ts +213 -0
  62. package/dist/validators-B7oIJCAp.js +279 -0
  63. package/dist/validators-vzRKjBJC.d.ts +88 -0
  64. package/dist/watcher.mjs +96 -0
  65. package/dist/where-clause-compiler-DdjN63Io.d.ts +4756 -0
  66. package/package.json +107 -35
  67. package/skills/convex/SKILL.md +486 -0
  68. package/skills/convex/references/features/aggregates.md +353 -0
  69. package/skills/convex/references/features/auth-admin.md +446 -0
  70. package/skills/convex/references/features/auth-organizations.md +1141 -0
  71. package/skills/convex/references/features/auth-polar.md +579 -0
  72. package/skills/convex/references/features/auth.md +470 -0
  73. package/skills/convex/references/features/create-plugins.md +153 -0
  74. package/skills/convex/references/features/http.md +676 -0
  75. package/skills/convex/references/features/migrations.md +162 -0
  76. package/skills/convex/references/features/orm.md +1166 -0
  77. package/skills/convex/references/features/react.md +657 -0
  78. package/skills/convex/references/features/scheduling.md +267 -0
  79. package/skills/convex/references/features/testing.md +209 -0
  80. package/skills/convex/references/setup/auth.md +501 -0
  81. package/skills/convex/references/setup/biome.md +190 -0
  82. package/skills/convex/references/setup/doc-guidelines.md +145 -0
  83. package/skills/convex/references/setup/index.md +759 -0
  84. package/skills/convex/references/setup/next.md +116 -0
  85. package/skills/convex/references/setup/react.md +175 -0
  86. package/skills/convex/references/setup/server.md +473 -0
  87. package/skills/convex/references/setup/start.md +67 -0
  88. package/LICENSE +0 -21
  89. package/README.md +0 -0
  90. package/dist/index.d.mts +0 -5
  91. package/dist/index.d.mts.map +0 -1
  92. package/dist/index.mjs +0 -6
  93. 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 |