Interactive Polar.sh billing integration wizard. Sets up products, webhooks, database schema, and subscription middleware for Cloudflare Workers.
Interactive wizard that integrates Polar.sh billing into Cloudflare Workers. Use this when setting up subscriptions, products, and webhook handlers for your SaaS app.
/plugin marketplace add hirefrank/hirefrank-marketplace/plugin install edge-stack@hirefrank-marketplace<command_purpose> Guide developers through complete Polar.sh billing integration with automated code generation, database migrations, and MCP-driven product configuration. </command_purpose>
<role>Senior Payments Integration Engineer with expertise in Polar.sh, Cloudflare Workers, and subscription management</role>
This command will:
<task_list>
</task_list>
Check Polar Products:
// Query MCP for products
const products = await mcp.polar.listProducts();
if (products.length === 0) {
console.log("ā ļø No products found in your Polar account");
console.log("š Next steps:");
console.log("1. Go to https://polar.sh/dashboard");
console.log("2. Create your products (Pro, Enterprise, etc.)");
console.log("3. Run this command again");
process.exit(0);
}
// Display products
console.log("ā
Found Polar products:");
products.forEach((p, i) => {
console.log(`${i + 1}. ${p.name} - $${p.prices[0].amount / 100}/${p.prices[0].interval}`);
console.log(` ID: ${p.id}`);
});
Generate File: app/routes/api/webhooks/polar.ts (Tanstack Start) or src/webhooks/polar.ts (Hono)
// Generated webhook handler
import { Polar } from '@polar-sh/sdk';
export interface Env {
POLAR_ACCESS_TOKEN: string;
POLAR_WEBHOOK_SECRET: string;
DB: D1Database;
}
export async function handlePolarWebhook(
request: Request,
env: Env
): Promise<Response> {
// 1. Verify webhook signature
const signature = request.headers.get('polar-signature');
if (!signature) {
return new Response('Missing signature', { status: 401 });
}
const body = await request.text();
const polar = new Polar({ accessToken: env.POLAR_ACCESS_TOKEN });
let event;
try {
event = polar.webhooks.verify(body, signature, env.POLAR_WEBHOOK_SECRET);
} catch (err) {
console.error('Webhook verification failed:', err);
return new Response('Invalid signature', { status: 401 });
}
// 2. Log event for debugging
await env.DB.prepare(
\`INSERT INTO webhook_events (id, type, data, created_at)
VALUES (?, ?, ?, ?)\`
).bind(
crypto.randomUUID(),
event.type,
JSON.stringify(event.data),
new Date().toISOString()
).run();
// 3. Handle event types
try {
switch (event.type) {
case 'checkout.completed':
await handleCheckoutCompleted(event.data, env);
break;
case 'subscription.created':
await handleSubscriptionCreated(event.data, env);
break;
case 'subscription.updated':
await handleSubscriptionUpdated(event.data, env);
break;
case 'subscription.canceled':
await handleSubscriptionCanceled(event.data, env);
break;
case 'subscription.past_due':
await handleSubscriptionPastDue(event.data, env);
break;
default:
console.log('Unhandled event type:', event.type);
}
return new Response('OK', { status: 200 });
} catch (err) {
console.error('Webhook processing error:', err);
return new Response('Processing failed', { status: 500 });
}
}
// Event handlers
async function handleCheckoutCompleted(data: any, env: Env) {
const { customer_id, product_id, metadata } = data;
await env.DB.prepare(
\`UPDATE users
SET polar_customer_id = ?,
product_id = ?,
subscription_status = 'active',
updated_at = ?
WHERE id = ?\`
).bind(customer_id, product_id, new Date().toISOString(), metadata.user_id).run();
}
async function handleSubscriptionCreated(data: any, env: Env) {
const { id, customer_id, product_id, status, current_period_end } = data;
await env.DB.prepare(
\`INSERT INTO subscriptions (id, polar_customer_id, product_id, status, current_period_end, created_at)
VALUES (?, ?, ?, ?, ?, ?)\`
).bind(id, customer_id, product_id, status, current_period_end, new Date().toISOString()).run();
}
async function handleSubscriptionUpdated(data: any, env: Env) {
const { id, status, product_id, current_period_end } = data;
await env.DB.prepare(
\`UPDATE subscriptions
SET status = ?, product_id = ?, current_period_end = ?, updated_at = ?
WHERE id = ?\`
).bind(status, product_id, current_period_end, new Date().toISOString(), id).run();
}
async function handleSubscriptionCanceled(data: any, env: Env) {
const { id } = data;
await env.DB.prepare(
\`UPDATE subscriptions
SET status = 'canceled', canceled_at = ?, updated_at = ?
WHERE id = ?\`
).bind(new Date().toISOString(), new Date().toISOString(), id).run();
}
async function handleSubscriptionPastDue(data: any, env: Env) {
const { id } = data;
await env.DB.prepare(
\`UPDATE subscriptions
SET status = 'past_due', updated_at = ?
WHERE id = ?\`
).bind(new Date().toISOString(), id).run();
// TODO: Send payment failure notification
console.log('Subscription past due:', id);
}
// App-specific export
export default defineEventHandler(async (event) => {
return await handlePolarWebhook(
event.node.req,
event.context.cloudflare.env
);
});
Generate File: migrations/0001_polar_billing.sql
-- Users table (add Polar fields)
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
polar_customer_id TEXT UNIQUE,
product_id TEXT,
subscription_status TEXT, -- 'active', 'canceled', 'past_due', NULL
current_period_end TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
-- Subscriptions table (detailed tracking)
CREATE TABLE subscriptions (
id TEXT PRIMARY KEY, -- Polar subscription ID
polar_customer_id TEXT NOT NULL,
product_id TEXT NOT NULL,
price_id TEXT NOT NULL,
status TEXT NOT NULL, -- 'active', 'canceled', 'past_due', 'trialing'
current_period_start TEXT,
current_period_end TEXT,
canceled_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (polar_customer_id) REFERENCES users(polar_customer_id)
);
-- Webhook events log (debugging/auditing)
CREATE TABLE webhook_events (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
data TEXT NOT NULL, -- JSON blob
created_at TEXT NOT NULL
);
-- Indexes for performance
CREATE INDEX idx_users_polar_customer ON users(polar_customer_id);
CREATE INDEX idx_users_subscription_status ON users(subscription_status);
CREATE INDEX idx_subscriptions_customer ON subscriptions(polar_customer_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
CREATE INDEX idx_webhook_events_type ON webhook_events(type);
CREATE INDEX idx_webhook_events_created ON webhook_events(created_at);
Run Migration:
wrangler d1 migrations apply DB --local
wrangler d1 migrations apply DB --remote
Generate File: app/middleware/subscription.ts (Tanstack Start) or src/middleware/subscription.ts (Hono)
// Subscription check middleware
export async function requireActiveSubscription(
request: Request,
env: Env,
ctx?: ExecutionContext
) {
// Get user ID from session (assumes auth is already set up)
const userId = await getUserIdFromSession(request, env);
if (!userId) {
return new Response('Unauthorized', { status: 401 });
}
// Check subscription status
const user = await env.DB.prepare(
\`SELECT subscription_status, current_period_end, product_id
FROM users
WHERE id = ?\`
).bind(userId).first();
if (!user) {
return new Response('User not found', { status: 404 });
}
// Check if subscription is active
if (user.subscription_status !== 'active') {
return new Response(JSON.stringify({
error: 'subscription_required',
message: 'Active subscription required to access this feature',
upgrade_url: '/pricing'
}), {
status: 403,
headers: { 'Content-Type': 'application/json' }
});
}
// Check if subscription hasn't expired
if (user.current_period_end) {
const periodEnd = new Date(user.current_period_end);
if (periodEnd < new Date()) {
return new Response(JSON.stringify({
error: 'subscription_expired',
message: 'Your subscription has expired',
renew_url: '/pricing'
}), {
status: 403,
headers: { 'Content-Type': 'application/json' }
});
}
}
// Subscription is valid, continue
return null;
}
// Helper to get user ID from session
async function getUserIdFromSession(request: Request, env: Env): Promise<string | null> {
// TODO: Implement based on your auth setup
// const session = await getUserSession(event);
// return session?.user?.id || null;
// For better-auth:
// const session = await auth.api.getSession({ headers: request.headers });
// return session?.user?.id || null;
return null; // Placeholder
}
Usage Example:
// Protected API route
export default defineEventHandler(async (event) => {
// Check subscription
const subscriptionCheck = await requireActiveSubscription(
event.node.req,
event.context.cloudflare.env
);
if (subscriptionCheck) {
return subscriptionCheck; // Return 403 if no subscription
}
// User has active subscription, proceed
return {
message: 'Premium feature accessed',
data: '...'
};
});
Update: wrangler.toml
# Add Polar webhook secret (public, not sensitive)
[vars]
POLAR_WEBHOOK_SECRET = "whsec_..." # Get from Polar dashboard
# D1 database (if not already configured)
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "..." # Get from: wrangler d1 create my-app-db
Create: .dev.vars (local development)
# Polar Access Token (sensitive - DO NOT COMMIT)
POLAR_ACCESS_TOKEN=polar_at_xxxxxxxxxxxxx
# Get this from: https://polar.sh/dashboard/settings/api
Production Setup:
# Set secret in Cloudflare Workers
wrangler secret put POLAR_ACCESS_TOKEN
# Paste: polar_at_xxxxxxxxxxxxx
Instructions for User:
## Configure Polar Webhook
1. Go to https://polar.sh/dashboard/settings/webhooks
2. Click "Add Webhook Endpoint"
3. Enter your webhook URL:
- Development: http://localhost:3000/api/webhooks/polar
- Production: https://yourdomain.com/api/webhooks/polar
4. Select events to send:
ā
checkout.completed
ā
subscription.created
ā
subscription.updated
ā
subscription.canceled
ā
subscription.past_due
5. Copy the "Webhook Secret" (whsec_...)
6. Add to wrangler.toml: POLAR_WEBHOOK_SECRET = "whsec_..."
7. Click "Create Endpoint"
8. Test with "Send Test Event" button
Validation Checklist:
// Run validation checks
const validation = {
polarAccount: await mcp.polar.verifySetup(),
products: await mcp.polar.listProducts(),
webhookEvents: await mcp.polar.getWebhookEvents(),
database: await checkDatabaseSchema(env),
environment: await checkEnvironmentVars(env),
webhookEndpoint: await checkWebhookHandler()
};
console.log("š Polar.sh Integration Validation\n");
// 1. Polar Account
console.log("ā
Polar Account:", validation.polarAccount.status);
console.log(` Found ${validation.products.length} products`);
// 2. Database Schema
if (validation.database.users && validation.database.subscriptions) {
console.log("ā
Database Schema: Complete");
} else {
console.log("ā Database Schema: Missing tables");
console.log(" Run: wrangler d1 migrations apply DB");
}
// 3. Environment Variables
if (validation.environment.POLAR_ACCESS_TOKEN && validation.environment.POLAR_WEBHOOK_SECRET) {
console.log("ā
Environment Variables: Configured");
} else {
console.log("ā Environment Variables: Missing");
if (!validation.environment.POLAR_ACCESS_TOKEN) {
console.log(" Missing: POLAR_ACCESS_TOKEN");
}
if (!validation.environment.POLAR_WEBHOOK_SECRET) {
console.log(" Missing: POLAR_WEBHOOK_SECRET");
}
}
// 4. Webhook Handler
if (validation.webhookEndpoint.exists) {
console.log("ā
Webhook Handler: Exists");
} else {
console.log("ā Webhook Handler: Not found");
}
console.log("\nš Next Steps:");
console.log("1. Configure webhook in Polar dashboard");
console.log("2. Test webhook with Polar's 'Send Test Event'");
console.log("3. Implement subscription checks on protected routes");
console.log("4. Deploy to production with: /es-deploy");
ā Billing setup complete when:
Files Created:
server/api/webhooks/polar.ts (or src/webhooks/polar.ts)server/middleware/subscription.ts (or src/middleware/subscription.ts)migrations/0001_polar_billing.sql.dev.vars (template)Files Updated:
wrangler.toml (added Polar vars and D1 binding)Next Actions:
/es-deployagents/integrations/polar-billing-specialist for detailed implementation guidance