Payments & Subscriptions
SaaS Box leverages Polar to handle everything related to payments, subscriptions, and checkouts. I built this integration to be as lightweight and developer-friendly as possible, relying on native webhooks to sync data securely.
Architecture Philosophy
When deciding how to handle payments in this boilerplate, I intentionally moved away from complex setups that require multiple database tables for invoices, subscriptions, and customers. Instead, I opted for an MVP-first approach using Polar as the Merchant of Record.
Merchant of Record (MoR)
What is a Merchant of Record? A MoR is the legal entity responsible for selling products to your customers. This means Polar assumes all liability for collecting global taxes (like VAT), issuing invoices, and handling compliance. I don't have to write any code for international tax laws.
Single Column State
Rather than mirroring complex subscription objects in my database, I distill the user's access level down to a single text column: their current plan.
Polar Setup & Sandbox
Follow these steps to initialize your products and get the necessary API keys securely.
The Sandbox Environment
Polar provides a fully featured Sandbox environment for safe testing without using real credit cards. In the top left corner of your Polar dashboard, make sure to toggle Sandbox to the ON position while developing. You will need to create your products and webhooks specifically within this Sandbox mode before launching to production.
Generate Organization Access Tokens
Under Settings > Developers , generate a new Organization Access Token. Copy this immediately. Because you are in Sandbox mode, this token should start withpolar_oat_. This token allows our serverless functions to authenticate with Polar securely.
Environment Config
Your application requires these keys to be set in your .env file. They are strictly validated by Zod inside src/env.ts.
Database Schema
How I handle subscription states locally in Drizzle ORM.
Instead of creating a massive, complex table structure to map every aspect of a subscription, I designed the user table to be incredibly straightforward. I added a single plancolumn that defaults to "free".
Webhooks Architecture
Understanding the golden rule of mapping Polar customers to local users.
What is a Webhook?
Think of a webhook as a "reverse API". Instead of my Next.js server asking Polar, "Did this user pay?", Polar actively calls my Next.js server to say, "Hey! A subscription was just created!" I use this notification to update the database immediately.
The External ID Mapping
When a user clicks a checkout link on our site, we must tell Polar who is paying. In Polar, a Customer represents a buyer. When creating a checkout session, we pass our local database user.id to Polar as the externalId.
Later, when the webhook fires, Polar tells us the customerId. We then fetch that customer from Polar, retrieve the externalId we saved earlier, and we instantly know exactly which user in our database needs to be upgraded!
Configuring the Webhook
How to expose your local machine and obtain the webhook secret.
Exposing Localhost with Ngrok
Because Polar needs to send an HTTP POST request to your application, it needs a public URL. When you are coding on localhost:3000, Polar cannot reach you. I use Ngrok to create a secure tunnel. Open two terminals and run:
Obtaining the Webhook Secret
Take the forwarding URL Ngrok gives you, append /api/webhook/polar, and paste it into your Polar Sandbox Dashboard under Settings > Webhooks . Make sure to check the events related to subscriptions (created, updated, revoked).
Once you click Create Webhook , Polar will generate a specific secret for this endpoint. It usually starts with polar_wh_. Copy this value and paste it into your .env file as thePOLAR_WEBHOOK_SECRET. This allows the application to cryptographically verify that incoming requests are genuinely from Polar.
The Webhook Handler
Polar provides a phenomenal official Next.js SDK. Instead of manually parsing headers and verifying cryptographic signatures, I use the Webhookshelper from @polar-sh/nextjs. This file lives at src/app/api/webhook/polar/route.ts.
Protecting Routes
Checking if a user has paid for premium features.
Server Actions & API Routes
Because our webhook keeps the database perfectly in sync, verifying access is as fast as a single database query. I don't need to ask Polar's API every time a user loads a page.
Frequently Asked Questions
Why not use Stripe directly?
Stripe is incredibly powerful, but requires you to manage tax calculation logic, receipt generation, and global compliance yourself unless you use Stripe Tax (which adds complexity). Polar handles all Merchant of Record duties automatically.
Can I have multiple paid tiers?
Yes! Just expand the plan column in the database schema to accept more string literals (e.g., "basic", "pro", "enterprise") and update the webhook logic to assign the correct string based on the Product ID returned in the payload.
Official Documentation
Dive deeper into the Polar ecosystem to learn about their full API surface and advanced checkouts.