Skip to content

feat: Integrate Polar.sh#142

Closed
Jackson57279 wants to merge 1 commit intomasterfrom
tembo/polar-stack-auth-perf
Closed

feat: Integrate Polar.sh#142
Jackson57279 wants to merge 1 commit intomasterfrom
tembo/polar-stack-auth-perf

Conversation

@Jackson57279
Copy link
Owner

@Jackson57279 Jackson57279 commented Nov 21, 2025

Description

Integrates polar.sh with Stack Auth to enable checkout functionality and manage subscriptions efficiently.

Changes

Implemented Polar checkout creation, added a webhook handler for Polar events to sync subscriptions with Convex, and created a Polar client utility.


Want me to make any changes? Add a review or comment with @tembo and i'll get back to work!

tembo.io app.tembo.io


Note

Adds a Polar checkout API and a webhook handler that validates events and syncs subscription state to Convex via a shared Polar client.

  • Backend/API:
    • Checkout creation (src/app/api/polar/create-checkout/route.ts):
      • Authenticates via Stack Auth, validates productId, creates Polar checkout (successUrl, customerEmail, metadata.userId), returns checkout url.
    • Webhooks (src/app/api/webhooks/polar/route.ts):
      • Verifies signatures with POLAR_WEBHOOK_SECRET, handles subscription.created|updated|active|revoked|canceled.
      • Maps Polar statuses and upserts/updates Convex via api.subscriptions.*; fetches product name when needed.
    • Polar client (src/lib/polar.ts):
      • Singleton getPolarClient using POLAR_ACCESS_TOKEN; selects sandbox/production by NODE_ENV.

Written by Cursor Bugbot for commit 9c0f620. Configure here.

Summary by CodeRabbit

Release Notes

  • New Features

    • Implemented real Polar checkout integration with secure payment processing and enhanced error handling
    • Added automatic webhook processing for subscription lifecycle events (creation, updates, status changes, revocation, cancellation)
    • Enabled customer and subscription tracking linked to user accounts
  • Chores

    • Added environment-aware configuration for seamless development and production deployment

✏️ Tip: You can customize this high-level summary in your review settings.

Co-authored-by: otdoges <otdoges@proton.me>
@vercel
Copy link

vercel bot commented Nov 21, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
zapdev Error Error Nov 21, 2025 3:51am

@codecapyai
Copy link

codecapyai bot commented Nov 21, 2025

CodeCapy Review ₍ᐢ•(ܫ)•ᐢ₎

Codebase Summary

ZapDev is an AI-powered development platform built with Next.js and a modern tech stack (React, TypeScript, Tailwind CSS, and more). It enables real-time code generation and sandboxed execution with a conversational interface. The repository includes features like authentication with Clerk, subscription management, and project persistence among others.

PR Changes

This PR integrates polar.sh with Stack Auth to enable checkout functionality and manage subscriptions more effectively. It introduces a new Polar checkout creation endpoint, a webhook handler for syncing subscription events with Convex, and a Polar client utility in the codebase.

Setup Instructions

  1. Install pnpm globally by running: sudo npm install -g pnpm
  2. Clone the repository and navigate to its root directory.
  3. Install dependencies by running: pnpm install
  4. Set up environment variables by copying env.example to .env and configuring the required values (including POLAR_ACCESS_TOKEN, NEXT_PUBLIC_APP_URL, and others).
  5. Start the development server using: pnpm dev
  6. Open your browser and navigate to http://localhost:3000 to verify the application is running.

Generated Test Cases

1: Successful Polar Checkout Creation ❗️❗️❗️

Description: Tests the complete user journey of initiating a Polar checkout. This includes verifying that a logged-in user can trigger the checkout process with a valid product ID, and the system successfully creates a checkout session and redirects the user to the provided checkout URL.

Prerequisites:

  • User is logged in
  • A valid product ID is available on the checkout page
  • POLAR_ACCESS_TOKEN and required environment variables are set in the test environment

Steps:

  1. Launch the application in a browser by navigating to http://localhost:3000.
  2. Log in using valid user credentials.
  3. Navigate to the checkout page where the 'Polar Checkout' button is displayed.
  4. Verify that the checkout button is clearly visible and active.
  5. Click the 'Polar Checkout' button. (Ensure that the button includes the productId, either selected from a list or embedded in the component.)
  6. Intercept the network call to POST /api/polar/create-checkout to ensure it sends the correct 'productId' and user metadata.
  7. Verify that the server responds with a JSON object containing a 'url' field.
  8. Confirm that the browser is redirected to the checkout URL returned by the API (the URL should include '/dashboard?checkout=success' as part of its query parameters).

Expected Result: The user sees a successful redirection to the checkout URL provided in the API response, confirming that the Polar checkout session was created successfully.

2: Handle Missing Product ID Error in Polar Checkout ❗️❗️

Description: Tests the error handling when a checkout request is made without a required productId. This scenario verifies that the system returns an appropriate error message and status code, and that the UI informs the user of the missing information.

Prerequisites:

  • User is logged in
  • The checkout page is accessible
  • A scenario is set up (e.g., through misconfigured UI input) where the productId is missing

Steps:

  1. Launch the application in a browser by navigating to http://localhost:3000.
  2. Log in using valid user credentials.
  3. Navigate to the checkout page where the Polar checkout process is initiated.
  4. Simulate a scenario where no product ID is provided (this could be achieved by clearing the input or altering UI state using dev tools).
  5. Click the 'Polar Checkout' button.
  6. Intercept the network request to POST /api/polar/create-checkout to confirm that the payload does not include a productId.
  7. Verify that the API response returns a 400 status with an error message indicating 'Missing productId'.
  8. Check that the UI displays an error message to the user informing them that a product must be selected or provided before proceeding.

Expected Result: The user remains on the same page and sees an error message stating 'Missing productId', ensuring that the system does not proceed with an incomplete checkout request.

3: Display Error When Polar Checkout Session Creation Fails ❗️❗️❗️

Description: Tests the user experience when the backend fails to create a Polar checkout session due to unexpected issues (e.g., network error, invalid POLAR_ACCESS_TOKEN). This test ensures that errors are caught, logged, and communicated to the user via the UI.

Prerequisites:

  • User is logged in
  • A valid product ID is provided
  • Simulate a failure condition on the backend (e.g., by temporarily using an invalid POLAR_ACCESS_TOKEN or intercepting the API call to force an error)

Steps:

  1. Launch the application in a browser by navigating to http://localhost:3000.
  2. Log in using valid user credentials.
  3. Navigate to the checkout page where the Polar Checkout button is visible.
  4. Set up the environment or intercept the network request so that the POLAR checkout API call fails (simulate a network error or invalid configuration).
  5. Click the 'Polar Checkout' button.
  6. Intercept and verify that the API call returns an error response (e.g., status 500 with a relevant error message such as 'Error creating Polar checkout session').
  7. Observe that the UI displays an appropriate error notification or message to the user regarding the failure.
  8. Ensure that the user is not redirected and remains on the checkout page.

Expected Result: The UI correctly displays an error message indicating that there was an issue creating the checkout session, and no redirection occurs.

Raw Changes Analyzed
File: src/app/api/polar/create-checkout/route.ts
Changes:
@@ -1,8 +1,7 @@
 import { NextRequest, NextResponse } from "next/server";
 import { getUser } from "@/lib/stack-auth";
+import { getPolarClient } from "@/lib/polar";
 
-// NOTE: Polar checkout will be implemented after Stack Auth is fully configured
-// This is a placeholder route for now
 export async function POST(req: NextRequest) {
   try {
     // Authenticate user with Stack Auth
@@ -15,11 +14,31 @@ export async function POST(req: NextRequest) {
       );
     }
 
-    // TODO: Implement Polar checkout once Stack Auth is configured with proper API keys
-    return NextResponse.json(
-      { error: "Polar checkout not yet configured. Please set up Stack Auth first." },
-      { status: 501 }
-    );
+    const body = await req.json();
+    const { productId } = body;
+
+    if (!productId) {
+      return NextResponse.json(
+        { error: "Missing productId" },
+        { status: 400 }
+      );
+    }
+
+    const polar = getPolarClient();
+    const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
+    
+    // Create checkout
+    // We pass userId in metadata so we can link subscription to user in webhook
+    const checkout = await polar.checkouts.create({
+      productId,
+      successUrl: `${appUrl}/dashboard?checkout=success`,
+      customerEmail: user.primaryEmail || undefined,
+      metadata: {
+        userId: user.id,
+      },
+    });
+
+    return NextResponse.json({ url: checkout.url });
   } catch (error) {
     console.error("Error creating Polar checkout session:", error);
     

File: src/app/api/webhooks/polar/route.ts
Changes:
@@ -0,0 +1,106 @@
+import { NextRequest, NextResponse } from "next/server";
+import { validateEvent } from "@polar-sh/sdk/webhooks";
+import { ConvexHttpClient } from "convex/browser";
+import { api } from "@/convex/_generated/api";
+import { getPolarClient } from "@/lib/polar";
+
+const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
+
+export async function POST(req: NextRequest) {
+  const requestBody = await req.text();
+  const webhookHeaders: Record<string, string> = {};
+  
+  req.headers.forEach((value, key) => {
+    webhookHeaders[key] = value;
+  });
+
+  const webhookSecret = process.env.POLAR_WEBHOOK_SECRET;
+
+  if (!webhookSecret) {
+    console.error("POLAR_WEBHOOK_SECRET is missing");
+    return NextResponse.json({ error: "Configuration error" }, { status: 500 });
+  }
+
+  let event;
+  try {
+    event = validateEvent(requestBody, webhookHeaders, webhookSecret);
+  } catch (error) {
+    console.error("Webhook verification failed:", error);
+    return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
+  }
+
+  try {
+    switch (event.type) {
+      case "subscription.created":
+      case "subscription.updated":
+      case "subscription.active": {
+        const subscription = event.data;
+        const userId = subscription.metadata?.userId as string | undefined;
+
+        if (!userId) {
+          console.warn(`Subscription ${subscription.id} missing userId in metadata`);
+          return NextResponse.json({ received: true });
+        }
+
+        // Try to get product name from payload or fetch it
+        let productName = (subscription as any).product?.name;
+        
+        if (!productName) {
+          try {
+            const polar = getPolarClient();
+            // Polar SDK types might differ, but usually there's a way to get product
+            const product = await polar.products.get({ id: subscription.product_id });
+            productName = product.name;
+          } catch (e) {
+            console.error("Failed to fetch product details", e);
+            productName = "Subscription";
+          }
+        }
+
+        // Map Polar status to Convex status
+        let status: "incomplete" | "active" | "canceled" | "past_due" | "unpaid" = "active";
+        const s = subscription.status;
+        
+        if (["active", "trialing"].includes(s)) status = "active";
+        else if (["incomplete"].includes(s)) status = "incomplete";
+        else if (["past_due"].includes(s)) status = "past_due";
+        else if (["canceled", "incomplete_expired"].includes(s)) status = "canceled";
+        else if (["unpaid"].includes(s)) status = "unpaid";
+
+        await convex.mutation(api.subscriptions.createOrUpdateSubscription, {
+          userId: userId,
+          polarCustomerId: subscription.customer_id,
+          polarSubscriptionId: subscription.id,
+          productId: subscription.product_id,
+          productName: productName,
+          status: status,
+          currentPeriodStart: new Date(subscription.current_period_start).getTime(),
+          currentPeriodEnd: new Date(subscription.current_period_end).getTime(),
+          cancelAtPeriodEnd: subscription.cancel_at_period_end,
+          metadata: subscription.metadata,
+        });
+        break;
+      }
+
+      case "subscription.revoked":
+        await convex.mutation(api.subscriptions.revokeSubscription, {
+          polarSubscriptionId: event.data.id,
+        });
+        break;
+
+      case "subscription.canceled":
+        await convex.mutation(api.subscriptions.markSubscriptionForCancellation, {
+          polarSubscriptionId: event.data.id,
+        });
+        break;
+
+      default:
+        console.log(`Unhandled event type: ${event.type}`);
+    }
+  } catch (error) {
+    console.error("Error processing webhook:", error);
+    return NextResponse.json({ error: "Error processing webhook" }, { status: 500 });
+  }
+
+  return NextResponse.json({ received: true });
+}

File: src/lib/polar.ts
Changes:
@@ -0,0 +1,18 @@
+import { Polar } from "@polar-sh/sdk";
+
+let polarInstance: Polar | null = null;
+
+export const getPolarClient = () => {
+  if (!polarInstance) {
+    const accessToken = process.env.POLAR_ACCESS_TOKEN;
+    if (!accessToken) {
+      throw new Error("POLAR_ACCESS_TOKEN is missing");
+    }
+
+    polarInstance = new Polar({
+      accessToken,
+      server: process.env.NODE_ENV === "development" ? "sandbox" : "production",
+    });
+  }
+  return polarInstance;
+};

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 21, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This PR implements Polar payment integration by adding three components: a Polar API client utility for authenticated requests, a checkout creation endpoint that validates requests and generates secure checkout URLs, and a webhook handler that processes Polar subscription events and synchronizes them with the Convex database.

Changes

Cohort / File(s) Summary
Polar client setup
src/lib/polar.ts
Exports a memoized getPolarClient() function that instantiates and caches a Polar API client using POLAR_ACCESS_TOKEN from environment, with environment-aware server configuration (sandbox for development, production otherwise).
Checkout API endpoint
src/app/api/polar/create-checkout/route.ts
Replaces placeholder with real checkout creation flow: validates productId in request body, calls Polar client to create checkout with success URL and user metadata, returns checkout URL in response. Adds validation (400 for missing productId) and error handling (500 with details).
Webhook handler
src/app/api/webhooks/polar/route.ts
New POST endpoint verifying Polar webhook signatures using POLAR_WEBHOOK_SECRET. Processes subscription events (created, updated, active, revoked, canceled) by extracting user and product details, mapping Polar statuses to Convex-compatible formats, and invoking corresponding Convex mutations (createOrUpdateSubscription, revokeSubscription, markSubscriptionForCancellation). Includes robust error handling and default fallbacks for missing data.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CheckoutAPI as Checkout API
    participant Polar
    participant Webhook as Webhook Handler
    participant Convex as Convex DB

    rect rgba(100, 150, 255, 0.2)
    note right of User: Checkout Flow
    User->>CheckoutAPI: POST /api/polar/create-checkout<br/>{productId}
    activate CheckoutAPI
    CheckoutAPI->>CheckoutAPI: Validate productId
    CheckoutAPI->>Polar: Create checkout<br/>(productId, userEmail, metadata)
    activate Polar
    Polar-->>CheckoutAPI: Return checkout URL
    deactivate Polar
    CheckoutAPI-->>User: {url}
    deactivate CheckoutAPI
    end

    rect rgba(100, 200, 100, 0.2)
    note right of Polar: Webhook Flow
    Polar->>Webhook: POST /api/webhooks/polar<br/>(signed event)
    activate Webhook
    Webhook->>Webhook: Verify webhook signature
    Webhook->>Webhook: Extract event type & data
    alt subscription.created/updated/active
        Webhook->>Polar: Fetch product details
        activate Polar
        Polar-->>Webhook: Product info
        deactivate Polar
        Webhook->>Convex: createOrUpdateSubscription mutation
    else subscription.revoked
        Webhook->>Convex: revokeSubscription mutation
    else subscription.canceled
        Webhook->>Convex: markSubscriptionForCancellation mutation
    else default
        Webhook->>Webhook: Log unhandled event
    end
    activate Convex
    Convex-->>Webhook: Mutation result
    deactivate Convex
    Webhook-->>Polar: {status: 200}
    deactivate Webhook
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Webhook signature verification: Verify HMAC-SHA256 verification logic using POLAR_WEBHOOK_SECRET and proper header extraction
  • Event type handling: Confirm all subscription state transitions map correctly to Convex mutations and status enumerations
  • Field mapping: Validate Polar API response fields (e.g., productId, subscription status values) match Convex schema expectations
  • Error handling paths: Review fallback behaviors (e.g., default productName to "Subscription", handling missing userId)
  • Environment configuration: Ensure POLAR_ACCESS_TOKEN and POLAR_WEBHOOK_SECRET are properly validated and documented

Possibly related PRs

Poem

🐰 A rabbit hops with glee,
Polar payments now flow free,
Webhooks dance and checkouts sing,
Convex rows in springtime spring! 🌱💳

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tembo/polar-stack-auth-perf

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e8192db and 9c0f620.

📒 Files selected for processing (3)
  • src/app/api/polar/create-checkout/route.ts (2 hunks)
  • src/app/api/webhooks/polar/route.ts (1 hunks)
  • src/lib/polar.ts (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecapyai
Copy link

codecapyai bot commented Nov 21, 2025

🚀 Scrapybara Ubuntu instance started!

Interactive stream

⚠️ Error fetching GitHub variables, continuing setup:

status_code: 502, body: {'detail': "Error communicating with container: Client error '400 Bad Request' for url 'http://13.56.246.38:30051/env'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400"}

@claude
Copy link

claude bot commented Nov 21, 2025

Pull Request Review: Polar.sh Integration

Overall Assessment

This PR successfully integrates Polar.sh payment and subscription management with Stack Auth. The implementation is well-structured and follows good patterns. However, there are several important security, reliability, and code quality improvements needed before merging.

🔴 Critical Issues

1. Missing Webhook Idempotency (Security & Reliability)

Location: src/app/api/webhooks/polar/route.ts

The webhook handler lacks idempotency protection. If Polar retries the same webhook (network issues, timeouts), you'll process duplicate events leading to:

  • Duplicate database entries
  • Race conditions
  • Incorrect subscription states

Fix: Use the existing webhookEvents table from schema.ts:
```typescript
// Add at start of switch statement:
const idempotencyKey = `${event.data.id}:${event.type}`;
const existing = await convex.query(api.webhookEvents.getByKey, { idempotencyKey });
if (existing) {
return NextResponse.json({ received: true, cached: true });
}

// After successful processing:
await convex.mutation(api.webhookEvents.create, {
idempotencyKey,
provider: "polar",
eventType: event.type,
processedAt: Date.now(),
expiresAt: Date.now() + (5 * 60 * 1000), // 5 minutes
});
```

2. Race Condition in Subscription Creation (Data Integrity)

Location: src/app/api/webhooks/polar/route.ts:67-73

Multiple webhook events can arrive simultaneously (created + active). The current check-then-insert pattern has a race condition window.

Fix: Use a database constraint or atomic upsert. The Convex mutations already handle this correctly, but consider adding explicit transaction handling if needed.

3. Unvalidated User Input (Security)

Location: src/app/api/polar/create-checkout/route.ts:17

The productId is accepted directly from user input without validation. Attackers could:

  • Request non-existent products
  • Access products they shouldn't
  • Cause Polar API errors

Fix: Validate against a whitelist:
```typescript
const VALID_PRODUCT_IDS = [
process.env.POLAR_PRO_PRODUCT_ID!,
// Add other valid product IDs
];

if (!VALID_PRODUCT_IDS.includes(productId)) {
return NextResponse.json(
{ error: "Invalid product ID" },
{ status: 400 }
);
}
```

4. Missing Environment Variable Validation (Reliability)

Location: src/lib/polar.ts:7-10

Runtime errors if POLAR_ACCESS_TOKEN is missing. Should fail fast at build time or provide clearer error context.

Fix:
```typescript
export const getPolarClient = () => {
if (!polarInstance) {
const accessToken = process.env.POLAR_ACCESS_TOKEN;
if (!accessToken) {
throw new Error(
"POLAR_ACCESS_TOKEN environment variable is required. " +
"Set it in .env.local for development or in Vercel dashboard for production."
);
}
// ... rest of implementation
}
return polarInstance;
};
```

⚠️ High Priority Issues

5. Incomplete Error Handling in Webhook (Reliability)

Location: src/app/api/webhooks/polar/route.ts:98-101

Generic error catch doesn't distinguish between:

  • Temporary failures (retry)
  • Permanent failures (don't retry)
  • Validation errors

Recommendation: Return appropriate HTTP status codes:

  • 200: Success
  • 400: Validation error (don't retry)
  • 500: Temporary error (Polar will retry)

6. Missing Rate Limiting (Security)

Location: src/app/api/polar/create-checkout/route.ts

No rate limiting on checkout creation. Attackers could spam checkout requests.

Recommendation: Use the existing rateLimits table:
```typescript
// Check rate limit
const rateLimitKey = `checkout:${user.id}`;
const isRateLimited = await checkRateLimit(rateLimitKey, 5, 60000); // 5 per minute
if (isRateLimited) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
```

7. Unsafe Type Casting (Type Safety)

Location: src/app/api/webhooks/polar/route.ts:37, 46

```typescript
const userId = subscription.metadata?.userId as string | undefined;
let productName = (subscription as any).product?.name;
```

Recommendation: Add proper type guards or Zod schemas:
```typescript
import { z } from "zod";

const PolarMetadataSchema = z.object({
userId: z.string()
});

const userId = PolarMetadataSchema.parse(subscription.metadata).userId;
```

📋 Medium Priority Issues

8. Incomplete Status Mapping (Correctness)

Location: src/app/api/webhooks/polar/route.ts:60-68

The status mapping uses string literals in includes() but then has an else branch. What happens if Polar adds a new status?

Recommendation:
```typescript
const statusMap: Record<string, typeof status> = {
active: "active",
trialing: "active",
incomplete: "incomplete",
past_due: "past_due",
canceled: "canceled",
incomplete_expired: "canceled",
unpaid: "unpaid",
};

const status = statusMap[subscription.status] || "incomplete"; // Safe default
```

9. Missing Logging (Observability)

Add structured logging for:

  • Successful checkouts created
  • Webhook processing steps
  • Errors with context (user ID, subscription ID)

Recommendation: Use Sentry breadcrumbs or structured logging:
```typescript
console.log(`[POLAR_WEBHOOK] Processing ${event.type} for subscription ${subscription.id}`);
```

10. No Test Coverage (Quality)

No tests found for the new functionality. Critical paths that need tests:

  • Webhook signature validation
  • Duplicate event handling (idempotency)
  • Status mapping edge cases
  • Missing userId handling
  • Error scenarios

Recommendation: Add tests to `tests/` directory following existing patterns.

11. Product Fetch Error Handling (UX)

Location: src/app/api/webhooks/polar/route.ts:48-57

Falls back to generic "Subscription" name on error. This loses important information.

Recommendation: Either fail the webhook (return 500) or log prominently for manual review.

💡 Best Practice Improvements

12. Singleton Pattern Can Leak State

Location: src/lib/polar.ts:3

The singleton pattern works for same-environment requests but could leak state in serverless edge cases.

Recommendation: Consider creating fresh instances or using per-request caching.

13. Hard-coded URLs

Location: src/app/api/polar/create-checkout/route.ts:28

`successUrl: `${appUrl}/dashboard?checkout=success``

Recommendation: Make this configurable or handle multiple success scenarios.

14. Missing CORS Headers (If needed)

If webhooks come from Polar's servers, you might need CORS configuration. Verify Polar's webhook delivery mechanism.

15. Environment Selection Logic

Location: src/lib/polar.ts:14

`server: process.env.NODE_ENV === "development" ? "sandbox" : "production"`

Recommendation: Use explicit env var for more control:
```typescript
server: process.env.POLAR_ENVIRONMENT === "production" ? "production" : "sandbox"
```

This allows staging environments to use sandbox mode.

✅ What's Good

  1. Proper Authentication: Uses Stack Auth correctly with `getUser()`
  2. Schema Integration: Correctly leverages existing Convex schema
  3. Error Handling Structure: Basic try-catch blocks in place
  4. Webhook Verification: Uses Polar's official `validateEvent` function
  5. Clean Separation: Polar client properly abstracted to `lib/polar.ts`
  6. Status Mapping: Attempts to map Polar statuses to internal statuses

📝 Required Changes Before Merge

  1. ✅ Add webhook idempotency protection
  2. ✅ Validate productId against whitelist
  3. ✅ Add proper type safety (remove `as any`)
  4. ✅ Improve error handling with specific status codes
  5. ✅ Add rate limiting to checkout endpoint
  6. ✅ Add comprehensive error logging
  7. ✅ Write tests for critical paths

🎯 Recommended Changes

  1. Add monitoring/alerting for webhook failures
  2. Document required environment variables in .env.example
  3. Add JSDoc comments to exported functions
  4. Consider adding webhook retry mechanism for transient Convex errors
  5. Add integration test using Polar's webhook testing tool

📚 Documentation Needs

Per CLAUDE.md, all documentation should go in `explanations/`. Consider adding:

  • `explanations/POLAR_INTEGRATION.md` - Setup guide
  • Update `CLAUDE.md` environment variables section
  • Add Polar webhook URL to deployment checklist

Security Score: 6/10

  • ✅ Webhook signature validation
  • ✅ User authentication
  • ❌ No idempotency
  • ❌ No rate limiting
  • ❌ Unvalidated user input

Recommendation

Request Changes - Address critical security issues (#1, #3, #6) before merging. The implementation is solid but needs hardening for production use.


Great work on the integration! The code structure is clean and follows the project's patterns well. Once the security concerns are addressed, this will be a solid addition to the codebase.

@codecapyai
Copy link

codecapyai bot commented Nov 21, 2025

🔧 Setting up test environment...

Agent Steps

@codecapyai
Copy link

codecapyai bot commented Nov 21, 2025

❌ Something went wrong:

status_code: 502, body: {'detail': {'error': "Unexpected error: Error code: 404 - {'type': 'error', 'error': {'type': 'not_found_error', 'message': 'model: claude-3-5-sonnet-20241022'}, 'request_id': 'req_011CVLMWTDi4DimoFp6r1DJv'}", 'provider': 'anthropic', 'error_type': 'ProviderAPIError'}}

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on December 17

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

productName: productName,
status: status,
currentPeriodStart: new Date(subscription.current_period_start).getTime(),
currentPeriodEnd: new Date(subscription.current_period_end).getTime(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Invalid date conversion without null checks in webhook

The webhook handler converts subscription.current_period_start and subscription.current_period_end to timestamps without validating they exist or are valid date values. If these fields are undefined or null, new Date() creates an Invalid Date and .getTime() returns NaN. This NaN value is then passed to the Convex mutation, which expects valid numbers, resulting in corrupted subscription records. The webhook returns successfully (200 OK) despite the data corruption, masking the error from Polar's retry mechanism.

Fix in Cursor Fix in Web

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +91 to +94
case "subscription.canceled":
await convex.mutation(api.subscriptions.markSubscriptionForCancellation, {
polarSubscriptionId: event.data.id,
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cancel webhook leaves subscription active

In the Polar webhook handler the subscription.canceled event routes to markSubscriptionForCancellation, which only sets the cancelAtPeriodEnd flag (see convex/subscriptions.ts) and leaves the status untouched. When Polar fires this event after a subscription is actually canceled, our Convex record will stay at its previous status (often active), so entitlement checks will never see the cancellation and users retain access. The canceled event should update the subscription status (e.g., via createOrUpdateSubscription or revokeSubscription) rather than just scheduling cancellation.

Useful? React with 👍 / 👎.

@Jackson57279 Jackson57279 deleted the tembo/polar-stack-auth-perf branch December 3, 2025 08:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant