add Polar.sh as a payment provider / merchant of record
@vincanger How hot do you think this is? Does it make sense to do it for launch week?
since polar.sh currently only supports USD i think integrating paddle would be a more common MoR provider
hey @mschneider82 we're going to add both :) https://github.com/wasp-lang/open-saas/issues/445
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:
PaymentProcessorinterface: 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.waspas 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 Looks great! I think you covered all the bases here. Go go go!