--- description: Interactive Polar.sh billing integration wizard. Sets up products, webhooks, database schema, and subscription middleware for Cloudflare Workers. --- # Billing Setup Command Guide developers through complete Polar.sh billing integration with automated code generation, database migrations, and MCP-driven product configuration. ## Introduction Senior Payments Integration Engineer with expertise in Polar.sh, Cloudflare Workers, and subscription management **This command will**: - Query Polar MCP for existing products/subscriptions - Generate webhook handler with signature verification - Create D1 database schema for customers/subscriptions - Generate subscription middleware for protected routes - Configure environment variables - Validate setup via Polar MCP ## Prerequisites - Cloudflare Workers project (Tanstack Start or Hono) - Polar.sh account: https://polar.sh - D1 database configured in wrangler.toml (or will create) - Polar Access Token (will guide through obtaining) ## Main Tasks ### 1. Check Polar Account Setup First, verify user has Polar account and products created. Use Polar MCP to check for existing products. #### Immediate Actions: - [ ] Check if Polar MCP is available - [ ] Prompt user for Polar Access Token (if not in env) - [ ] Query Polar MCP for existing products - [ ] If no products found, guide to Polar dashboard - [ ] Display available products and let user select which to integrate **Check Polar Products**: ```typescript // 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}`); }); ``` ### 2. Generate Webhook Handler Create comprehensive webhook handler with signature verification and all critical event handlers. **Generate File**: `app/routes/api/webhooks/polar.ts` (Tanstack Start) or `src/webhooks/polar.ts` (Hono) ```typescript // 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 { // 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 ); }); ``` ### 3. Generate Database Migration Create D1 schema for users, subscriptions, and webhook event logging. **Generate File**: `migrations/0001_polar_billing.sql` ```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**: ```bash wrangler d1 migrations apply DB --local wrangler d1 migrations apply DB --remote ``` ### 4. Generate Subscription Middleware Create middleware to check subscription status on protected routes. **Generate File**: `app/middleware/subscription.ts` (Tanstack Start) or `src/middleware/subscription.ts` (Hono) ```typescript // 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 { // 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**: ```typescript // 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: '...' }; }); ``` ### 5. Configure Environment Variables Update wrangler.toml and create .dev.vars template. **Update**: `wrangler.toml` ```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) ```bash # Polar Access Token (sensitive - DO NOT COMMIT) POLAR_ACCESS_TOKEN=polar_at_xxxxxxxxxxxxx # Get this from: https://polar.sh/dashboard/settings/api ``` **Production Setup**: ```bash # Set secret in Cloudflare Workers wrangler secret put POLAR_ACCESS_TOKEN # Paste: polar_at_xxxxxxxxxxxxx ``` ### 6. Configure Polar Webhook Endpoint User needs to configure webhook endpoint in Polar dashboard. **Instructions for User**: ```markdown ## 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 ``` ### 7. Validate Setup Use Polar MCP to verify configuration is correct. **Validation Checklist**: ```typescript // 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"); ``` ## Success Criteria āœ… Billing setup complete when: - Polar products queried successfully via MCP - Webhook handler generated with signature verification - Database schema created (users, subscriptions, webhook_events) - Subscription middleware generated - Environment variables configured - Validation passes all checks - User guided through Polar dashboard configuration ## Output Summary **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**: 1. Run database migration 2. Configure webhook in Polar dashboard 3. Test webhook with Polar simulator 4. Add subscription checks to protected routes 5. Deploy with `/es-deploy` ## Notes - Always use Polar MCP for real-time product data - Test webhooks locally with Polar's test event feature - Store POLAR_ACCESS_TOKEN as Cloudflare secret (not in wrangler.toml) - Webhook endpoint must be publicly accessible (use ngrok for local testing) - See `agents/integrations/polar-billing-specialist` for detailed implementation guidance