open-saas icon indicating copy to clipboard operation
open-saas copied to clipboard

add Polar.sh as a payment provider / merchant of record

Open vincanger opened this issue 6 months ago • 5 comments

vincanger avatar Jun 19 '25 10:06 vincanger

@vincanger How hot do you think this is? Does it make sense to do it for launch week?

sodic avatar Jun 25 '25 13:06 sodic

since polar.sh currently only supports USD i think integrating paddle would be a more common MoR provider

mschneider82 avatar Jun 30 '25 09:06 mschneider82

hey @mschneider82 we're going to add both :) https://github.com/wasp-lang/open-saas/issues/445

vincanger avatar Jul 08 '25 10:07 vincanger

Polar Payment Integration for OpenSaaS Template

Overview

This issue outlines the implementation plan for integrating Polar as a new payment processor option in the OpenSaaS template, alongside the existing Stripe and LemonSqueezy integrations.

Current OpenSaaS Payment Architecture

The OpenSaaS template currently uses a clean abstraction pattern with:

  • PaymentProcessor interface: Standardised interface with methods and properties to interact with supported payment platforms
  • Pluggable implementation: Choose between Stripe or LemonSqueezy via configuration
  • Database schema: User model with payment fields like paymentProcessorUserId, subscriptionStatus, subscriptionPlan
  • Webhook integration: Configured in main.wasp as custom API endpoint at /payments-webhook
  • Payment plans: Centralised plan definitions supporting subscriptions and credits
  • Frontend components: Checkout page, pricing page and Wasp operations

Implementation Plan

1. Core Polar Integration

1.1 Install Polar Dependencies

npm install @polar-sh/sdk @polar-sh/express

1.2 Create Polar Directory Structure

src/payment/polar/
├── polarClient.ts         # Polar SDK client setup
├── paymentProcessor.ts    # PaymentProcessor interface implementation
├── checkoutUtils.ts       # Checkout session creation utilities
├── paymentDetails.ts      # User payment details update functions
├── webhook.ts            # Webhook event handlers
└── webhookPayload.ts     # Webhook payload parsing and validation

1.3 Environment Variables

Add required environment variables to .env.server:

POLAR_ACCESS_TOKEN=         # Polar API access token (sandbox for development)
POLAR_ORGANIZATION_ID=      # Polar organization ID
POLAR_WEBHOOK_SECRET=       # Webhook signing secret
POLAR_CUSTOMER_PORTAL_URL=  # Customer portal URL
POLAR_SANDBOX_MODE=true     # Enable sandbox mode for development

2. Polar Payment Processor Implementation

2.1 Polar Client Setup (polarClient.ts)

Configure the Polar client in a similar way to the Stripe client:

import { PolarApi } from '@polar-sh/sdk';
import { requireNodeEnvVar } from '../../server/utils';

export const polar = new PolarApi({
  accessToken: requireNodeEnvVar('POLAR_ACCESS_TOKEN'),
  // Enable sandbox mode during development
  sandbox: process.env.NODE_ENV === 'development' || process.env.POLAR_SANDBOX_MODE === 'true',
});

2.2 Payment Processor Implementation (paymentProcessor.ts)

Implement the PaymentProcessor interface using a hybrid approach:

  • createCheckoutSession: Use the checkouts.create SDK method for checkout session creation
  • fetchCustomerPortalUrl: Use the customerSession.customerPortalUrl SDK property to return the customer portal URL
  • webhook: Use the Webhooks utility from the Express adapter
  • webhookMiddlewareConfigFn: Configure middleware using the Polar SDK

2.3 Checkout Session Creation (checkoutUtils.ts)

Implement checkout session creation using the Polar SDK:

  • Customer creation/lookup
  • Checkout session creation with product/price IDs
  • Handle both subscription and one-time payment modes
  • Follow existing OpenSaaS patterns used in Stripe/LemonSqueezy implementations

2.4 Payment Details Updates (paymentDetails.ts)

Create utility functions to update user payment information:

export const updateUserPolarPaymentDetails = (
  { polarCustomerId, subscriptionPlan, subscriptionStatus, datePaid, numOfCreditsPurchased }: {
    polarCustomerId: string;
    subscriptionPlan?: PaymentPlanId;
    subscriptionStatus?: SubscriptionStatus;
    numOfCreditsPurchased?: number;
    datePaid?: Date;
  },
  userDelegate: PrismaClient['user']
) => {
  return userDelegate.update({
    where: {
      paymentProcessorUserId: polarCustomerId
    },
    data: {
      paymentProcessorUserId: polarCustomerId,
      subscriptionPlan,
      subscriptionStatus,
      datePaid,
      credits: numOfCreditsPurchased !== undefined ? { increment: numOfCreditsPurchased } : undefined,
    },
  });
};

3. Webhook Integration

3.1 Webhook Event Handlers (webhook.ts)

Use the Express adapter's Webhooks utility for preconfigured handling:

  • Import and configure the Express adapter webhook handler
  • Implement custom event handlers for key Polar webhook events:
    • checkout.session.completed: Handle successful checkout completion
    • subscription.created: Handle new subscription creation
    • subscription.updated: Handle subscription changes
    • subscription.cancelled: Handle subscription cancellation
    • order.created: Handle one-time payment completion

3.2 Webhook Payload Parsing (webhookPayload.ts)

Create Zod schemas for webhook payload validation:

const polarWebhookEventSchema = z.object({
  type: z.string(),
  data: z.object({
    // Polar webhook data structure
  }),
});

4. Payment Plans Configuration

4.1 Environment Variables for Plans

Add Polar product/price IDs to environment variables (use sandbox IDs for development):

POLAR_HOBBY_SUBSCRIPTION_PLAN_ID=    # Sandbox product ID for development
POLAR_PRO_SUBSCRIPTION_PLAN_ID=      # Sandbox product ID for development
POLAR_CREDITS_10_PLAN_ID=            # Sandbox product ID for development

4.2 Update Payment Plans (plans.ts)

Extend payment plan configuration to support Polar:

  • Add Polar-specific product ID environment variables
  • Ensure plan effects work with Polar's product structure

5. Frontend Integration

5.1 Checkout Flow

  • Polar checkout sessions should integrate seamlessly with existing CheckoutPage.tsx
  • Success/cancel URLs should redirect to existing OpenSaaS checkout result pages
  • No frontend changes needed if using Polar's hosted checkout

5.2 Customer Portal

  • Update customer portal links to use Polar's customer portal
  • Ensure subscription management works through Polar's portal

6. Configuration Updates

Update paymentProcessor.ts to include Polar option:

export interface PaymentProcessor {
  id: 'stripe' | 'lemonsqueezy' | 'polar';
  // Other interface members
}

7. Testing

7.1 Local Development

  • Set up Polar sandbox environment for testing
  • Configure webhook forwarding (similar to Stripe CLI or ngrok for LemonSqueezy)
  • Test checkout flow using Polar's sandbox environment
  • Use sandbox products and pricing for development testing

7.2 Integration Tests

  • Add Polar-specific test cases to existing e2e tests using sandbox environment
  • Test webhook event handling with sandbox webhook events
  • Verify user payment status updates with sandbox transactions

8. Documentation and Examples

8.1 Integration Documentation

Update Payments Integration documentation based on existing Stripe/LemonSqueezy guides:

  • Account setup instructions
  • API key retrieval
  • Creating test products
  • Webhook setup
  • Testing procedures using Polar's sandbox environment

8.2 Environment Variables Template

Update .env.server.example with Polar variables and instructions.

Implementation Considerations

  • Map Polar webhook events to OpenSaaS payment flow stages
  • Ensure subscription status updates align with existing status enums
  • Verify Polar's customer portal provides all features users expect
  • Test subscription management, payment method updates, etc.

Success Criteria

  • [ ] Polar payment processor implements all PaymentProcessor interface methods
  • [ ] Checkout flow works seamlessly with existing OpenSaaS frontend
  • [ ] Webhooks properly update user payment status and subscription information
  • [ ] Customer portal allows users to manage subscriptions
  • [ ] Both subscription and one-time payment (credits) models work
  • [ ] Integration matches the quality and completeness of existing Stripe/LemonSqueezy implementations
  • [ ] Documentation enables users to easily switch to Polar
  • [ ] Tests verify all payment scenarios work correctly

Genyus avatar Jul 10 '25 15:07 Genyus

@Genyus Looks great! I think you covered all the bases here. Go go go!

vincanger avatar Jul 22 '25 09:07 vincanger