saas-billing · diff
git:20260913.4cedcda to v3.0.0
383 added, 383 removed. Audit A to A.
- ---
- name: saas-billing
- description: "Implement and audit SaaS billing systems, subscription state machines, secure webhooks, and local database synchronization / Implementasi dan audit sistem billing SaaS, state machine langganan, webhook aman, dan sinkronisasi database lokal."
- author: "Roedy Rustam"
- ---
-
- # SaaS Billing Expert (2026 Edition)
-
- [English](#english) | [Bahasa Indonesia](#bahasa-indonesia)
-
- ---
-
- <a name="english"></a>
- ## English
-
- ### Orchestration & Integration
- Connects and orchestrates with relevant domain skills like `brainstorming`, `zero-to-prod-orchestrator`, and `session-memory-manager` to ensure cohesive execution.
-
- ### Description
- Expert guide for implementing and auditing SaaS billing systems. Covers subscription state machines, secure webhook handling, database synchronization, and the 2026 billing landscape including Stripe, **Polar.sh** (open-source, developer-first), **LemonSqueezy**, PayPal, and Midtrans (for Southeast Asia).
-
- ### Trigger Conditions
- - Integrating any payment gateway (Stripe, Polar.sh, LemonSqueezy, Midtrans, PayPal) into a SaaS application.
- - Using a Static-to-Dynamic QRIS alternative (with unique nominals and mutation webhooks) for local developers without PG accounts.
- - Implementing subscription state machines (active → past_due → canceled → reactivated).
- - Building secure webhook handlers with signature verification and idempotency.
- - Syncing external subscription status to a local database.
- - Implementing usage-based billing or metered API pricing.
- - Building the customer billing portal (manage subscription, download invoices).
- - Auditing an existing billing system for security gaps.
-
- ### 2026 Billing Provider Landscape
-
- | Provider | Best For | Open Source | Merchant of Record |
- |---|---|---|---|
- | **Stripe** | Enterprise, global, complex billing | ❌ | ❌ |
- | **Polar.sh** | Developer-first, open-source products | ✅ | ✅ (optional) |
- | **LemonSqueezy** | Indie hackers, simple pricing, global | ❌ | ✅ |
- | **Paddle** | B2B SaaS, EU VAT compliance | ❌ | ✅ |
- | **Midtrans** | Southeast Asia / Indonesia | ❌ | ❌ |
- | **PayPal** | Global, consumer trust | ❌ | ❌ |
-
- > **Merchant of Record (MoR)**: The provider handles tax compliance (VAT, GST), chargebacks, and legal liability — ideal for small teams without a finance department.
-
- ### Polar.sh — Developer-First Billing (2026 Rising Star)
- Polar.sh is the modern, open-source alternative to Gumroad/LemonSqueezy, purpose-built for developers and open-source projects:
- ```typescript
- import { Polar } from "@polar-sh/sdk";
-
- const polar = new Polar({ accessToken: process.env.POLAR_ACCESS_TOKEN });
-
- // Create a checkout session
- const checkout = await polar.checkouts.custom.create({
- productId: "prod_xxxx",
- successUrl: "https://myapp.com/success?checkout={CHECKOUT_ID}",
- customerEmail: user.email,
- metadata: { userId: user.id },
- });
-
- // Redirect to checkout
- return redirect(checkout.url);
- ```
-
- ```typescript
- // Webhook handler (Next.js App Router)
- import { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks";
-
- export async function POST(req: Request) {
- const body = await req.text();
- const signature = req.headers.get("webhook-signature") ?? "";
-
- try {
- const event = validateEvent(body, req.headers, process.env.POLAR_WEBHOOK_SECRET!);
-
- switch (event.type) {
- case "subscription.created":
- case "subscription.updated":
- await syncSubscription(event.data);
- break;
- case "subscription.canceled":
- await cancelSubscription(event.data.id);
- break;
- }
- return new Response(null, { status: 200 });
- } catch (e) {
- if (e instanceof WebhookVerificationError) {
- return new Response("Invalid signature", { status: 403 });
- }
- throw e;
- }
- }
- ```
-
- ### Stripe — Production Patterns
-
- #### Subscription State Machine
- ```
- FREE ──subscribe──> TRIALING ──trial_ends──> ACTIVE
- │
- ┌───────────────┤
- │ │
- payment fails cancel
- │ │
- PAST_DUE CANCELED
- │
- 3 failed retries
- │
- CANCELED
- ```
-
- #### Idempotent Webhook Handler
- ```typescript
- // app/api/webhooks/stripe/route.ts
- import Stripe from 'stripe';
- import { db } from '@/lib/db';
-
- const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
-
- export async function POST(req: Request) {
- const body = await req.text();
- const sig = req.headers.get('stripe-signature')!;
-
- let event: Stripe.Event;
- try {
- event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
- } catch {
- return new Response('Invalid signature', { status: 400 });
- }
-
- // Idempotency: skip already-processed events
- const processed = await db.webhookEvent.findUnique({ where: { stripeEventId: event.id } });
- if (processed) return new Response(null, { status: 200 });
-
- // Process event
- switch (event.type) {
- case 'customer.subscription.created':
- case 'customer.subscription.updated': {
- const sub = event.data.object as Stripe.Subscription;
- await db.subscription.upsert({
- where: { stripeSubId: sub.id },
- create: { stripeSubId: sub.id, status: sub.status, userId: sub.metadata.userId },
- update: { status: sub.status, currentPeriodEnd: new Date(sub.current_period_end * 1000) },
- });
- break;
- }
- case 'invoice.payment_failed': {
- const invoice = event.data.object as Stripe.Invoice;
- await sendDunningEmail(invoice.customer_email!);
- break;
- }
- }
-
- // Mark as processed
- await db.webhookEvent.create({ data: { stripeEventId: event.id } });
- return new Response(null, { status: 200 });
- }
- ```
-
- #### Usage-Based Billing (Metered)
- ```typescript
- // Report usage at end of billing period
- await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
- quantity: apiCallsThisMonth,
- timestamp: Math.floor(Date.now() / 1000),
- action: 'set', // 'set' or 'increment'
- });
- ```
-
- ### PayPal — Checkout Integration
-
- PayPal remains a trusted global standard for one-off payments and subscriptions.
-
- #### Create Order (Server-Side)
- ```typescript
- // app/api/paypal/create-order/route.ts
- import { paypalClient } from '@/lib/paypal';
- import paypal from '@paypal/checkout-server-sdk';
-
- export async function POST() {
- const request = new paypal.orders.OrdersCreateRequest();
- request.prefer("return=representation");
- request.requestBody({
- intent: 'CAPTURE',
- purchase_units: [{ amount: { currency_code: 'USD', value: '29.99' } }]
- });
-
- const response = await paypalClient().execute(request);
- return Response.json({ id: response.result.id });
- }
- ```
-
- #### Capture Payment (Server-Side)
- ```typescript
- // app/api/paypal/capture-order/route.ts
- import { db } from '@/lib/db';
- import { paypalClient } from '@/lib/paypal';
- import paypal from '@paypal/checkout-server-sdk';
-
- export async function POST(req: Request) {
- const { orderID, userId } = await req.json();
- const request = new paypal.orders.OrdersCaptureRequest(orderID);
- request.requestBody({});
-
- const response = await paypalClient().execute(request);
- if (response.result.status === 'COMPLETED') {
- // Grant access or update subscription in DB
- await db.subscription.create({
- data: { userId, provider: 'paypal', status: 'active' }
- });
- return Response.json({ success: true });
- }
- return Response.json({ success: false }, { status: 400 });
- }
- ```
-
- ### QRIS Static-to-Dynamic (Indonesia Alternative)
-
- For Indonesian developers without an official Payment Gateway account, you can create a "Dynamic" QRIS experience using a single Static QRIS combined with unique payment amounts and a bank mutation checking service (e.g., Moota, Cekmutasi) via webhook.
-
- #### 1. Generate Unique Amount (Server-Side)
- ```typescript
- // Add a unique 3-digit code to the base price
- export async function createQrisTransaction(userId: string, basePrice: number) {
- // Generate a random code between 1 and 999
- const uniqueCode = Math.floor(Math.random() * 999) + 1;
- const totalAmount = basePrice + uniqueCode;
-
- const transaction = await db.transaction.create({
- data: {
- userId, basePrice, uniqueCode, totalAmount,
- status: 'pending', provider: 'qris_static',
- expiresAt: new Date(Date.now() + 15 * 60 * 1000) // 15 mins expiry
- }
- });
-
- return {
- transactionId: transaction.id,
- totalAmount,
- qrisUrl: "https://myapp.com/static-qris.png" // User must manually input totalAmount
- };
- }
- ```
-
- #### 2. Mutation Webhook Handler & Real-Time Notification
- ```typescript
- // app/api/webhooks/mutation/route.ts
- import { db } from '@/lib/db';
- import { pusherServer } from '@/lib/pusher';
-
- export async function POST(req: Request) {
- const signature = req.headers.get("signature");
- // TODO: Verify signature from mutation service (e.g., Moota)
-
- const mutations = await req.json();
-
- for (const mutation of mutations) {
- if (mutation.type === 'CR' && mutation.amount > 0) {
- // Find pending transaction matching the exact unique amount
- const tx = await db.transaction.findFirst({
- where: {
- totalAmount: mutation.amount,
- status: 'pending',
- provider: 'qris_static',
- expiresAt: { gt: new Date() }
- }
- });
-
- if (tx) {
- // Mark as paid and activate subscription
- await db.transaction.update({ where: { id: tx.id }, data: { status: 'paid' } });
- await db.subscription.create({
- data: { userId: tx.userId, provider: 'qris_static', status: 'active' }
- });
-
- // Trigger real-time notification to frontend
- await pusherServer.trigger(`payment-${tx.id}`, 'payment-success', { success: true });
- }
- }
- }
-
- return new Response("OK", { status: 200 });
- }
- ```
-
- ### Database Schema for Multi-Provider Billing
- ```typescript
- // Drizzle ORM — supports Stripe, Polar, LemonSqueezy, PayPal, QRIS Static
- export const subscriptions = pgTable('subscriptions', {
- id: text('id').primaryKey(),
- workspaceId: text('workspace_id').references(() => workspaces.id).notNull(),
- provider: text('provider').$type<'stripe' | 'polar' | 'lemonsqueezy' | 'paypal' | 'qris_static'>().notNull(),
- externalCustomerId: text('external_customer_id').notNull(),
- externalSubId: text('external_sub_id').notNull().unique(),
- status: text('status').$type<'active' | 'trialing' | 'past_due' | 'canceled' | 'paused'>().notNull(),
- plan: text('plan').$type<'free' | 'pro' | 'enterprise'>().default('free').notNull(),
- currentPeriodEnd: timestamp('current_period_end'),
- cancelAtPeriodEnd: boolean('cancel_at_period_end').default(false),
- createdAt: timestamp('created_at').defaultNow().notNull(),
- updatedAt: timestamp('updated_at').defaultNow().notNull(),
- });
- ```
-
- ### Billing Security Checklist
- - [ ] Webhook signature verified on every request — reject without valid signature.
- - [ ] Webhook idempotency implemented — never process the same event twice.
- - [ ] Session Management Optimization: Secure the billing portal route with strict session validation and CSRF protection. Do not cache session-dependent billing states.
- - [ ] Use Stripe CLI / Polar.sh test webhooks for local development.
- - [ ] All billing API calls use server-side code only — never expose secret keys to frontend.
- - [ ] Plan limits enforced on every protected route (not just at checkout).
- - [ ] Failed payment dunning flow configured (email sequence, grace period).
- - [ ] Customer portal link available from within the app.
-
- ---
-
- <a name="bahasa-indonesia"></a>
- ## Bahasa Indonesia
-
- ### Integrasi Orkestrasi
- Terhubung dan mengorkestrasi skill domain yang relevan seperti `brainstorming`, `zero-to-prod-orchestrator`, dan `session-memory-manager` untuk memastikan eksekusi yang kohesif.
-
- ### Deskripsi
- Panduan ahli untuk mengimplementasikan dan mengaudit sistem billing SaaS. Mencakup state machine langganan, penanganan webhook aman, sinkronisasi database, dan lanskap billing 2026 termasuk Stripe, **Polar.sh** (open-source, developer-first), **LemonSqueezy**, PayPal, dan Midtrans (untuk Asia Tenggara).
-
- ### Kondisi Pemicu
- - Mengintegrasikan payment gateway (Stripe, Polar.sh, LemonSqueezy, Midtrans, PayPal) ke aplikasi SaaS.
- - Menggunakan alternatif QRIS Statis menjadi Dinamis (dengan nominal unik dan webhook mutasi) untuk developer lokal tanpa akun PG.
- - Mengimplementasikan state machine langganan.
- - Membangun webhook handler aman dengan verifikasi tanda tangan dan idempotency.
- - Menyinkronkan status langganan eksternal ke database lokal.
- - Mengimplementasikan billing berbasis penggunaan (metered pricing).
- - Membangun portal billing pelanggan.
- - Mengaudit sistem billing yang ada untuk celah keamanan.
-
- ### Lanskap Provider Billing 2026
-
- | Provider | Terbaik Untuk | Open Source | Merchant of Record |
- |---|---|---|---|
- | **Stripe** | Enterprise, global, billing kompleks | ❌ | ❌ |
- | **Polar.sh** | Developer-first, produk open-source | ✅ | ✅ (opsional) |
- | **LemonSqueezy** | Indie hackers, harga sederhana | ❌ | ✅ |
- | **Paddle** | B2B SaaS, kepatuhan PPN EU | ❌ | ✅ |
- | **Midtrans** | Asia Tenggara / Indonesia | ❌ | ❌ |
- | **PayPal** | Global, kepercayaan konsumen (consumer trust) | ❌ | ❌ |
-
- > **Merchant of Record (MoR)**: Provider menangani kepatuhan pajak (PPN, GST), chargeback, dan tanggung jawab hukum — ideal untuk tim kecil tanpa departemen keuangan.
-
- ### Polar.sh — Billing Developer-First
- Polar.sh adalah alternatif open-source modern untuk Gumroad/LemonSqueezy, dirancang khusus untuk developer dan proyek open-source. Mendukung checkout, webhook, dan manajemen langganan dengan SDK TypeScript yang bersih.
-
- ### PayPal — Integrasi Checkout
- PayPal sering digunakan sebagai gateway alternatif atau utama karena tingginya kepercayaan konsumen global.
- - **Server-Side Checkout**: Gunakan `create-order` dan `capture-order` di backend (menggunakan `@paypal/checkout-server-sdk`) untuk memastikan keamanan dan mencegah manipulasi harga di sisi klien.
- - **Webhook**: Verifikasi webhook dari PayPal untuk langganan yang diperbarui atau dibatalkan.
-
- ### Stripe — Pola Produksi
-
- #### State Machine Langganan
- Kelola transisi status: `FREE → TRIALING → ACTIVE → PAST_DUE → CANCELED → (reaktivasi)`.
-
- #### Webhook Handler Idempoten
- Selalu verifikasi tanda tangan webhook, tandai event sebagai diproses di database untuk mencegah duplikasi.
-
- #### Billing Berbasis Penggunaan (Metered)
- Laporkan penggunaan API dengan `stripe.subscriptionItems.createUsageRecord()` di akhir periode billing.
-
- ### Alternatif Lokal: QRIS Statis Rasa Dinamis (Tanpa Akun Payment Gateway)
- Bagi pengguna/developer di Indonesia yang belum memiliki Payment Gateway (seperti Midtrans), Anda dapat membuat pengalaman QRIS "Dinamis" menggunakan satu gambar QRIS statis biasa.
- - **Generate Nominal Unik (Endpoint)**: Tambahkan angka unik (misalnya 3 digit acak) ke harga dasar (contoh: Rp 100.000 menjadi Rp 100.123). Simpan ke database sebagai transaksi `pending` dengan batas waktu kadaluarsa (misal 15 menit). Tampilkan gambar QRIS beserta instruksi transfer sesuai nominal unik.
- - **Webhook Mutasi Bank**: Gunakan layanan pihak ketiga (seperti Moota, Cekmutasi) yang mengirimkan notifikasi webhook (ke `/api/webhooks/mutation`) setiap kali ada uang masuk.
- - **Validasi Otomatis**: Saat webhook menerima payload mutasi kredit (`CR`), sistem mencari transaksi `pending` yang jumlahnya sama persis (`totalAmount`). Jika cocok, sistem menandai tagihan sebagai lunas (`paid`) dan mengaktifkan langganan.
- - **Notifikasi Klien**: Gunakan WebSocket (seperti Pusher atau Socket.io) di backend untuk melakukan *trigger event* "pembayaran berhasil". Di frontend, *listen* ke event tersebut dan tampilkan notifikasi *real-time* kepada pengguna secara instan tanpa perlu me-refresh halaman.
-
- ### Skema Database Multi-Provider
- Rancang tabel `subscriptions` yang mendukung beberapa provider (`stripe`, `polar`, `lemonsqueezy`, `paypal`, `qris_static`) dengan kolom `provider` dan ID eksternal yang terpisah.
-
- ### Checklist Keamanan Billing
- - [ ] Tanda tangan webhook diverifikasi pada setiap permintaan.
- - [ ] Idempotency webhook diimplementasikan.
- - [ ] Optimasi Session Management: Amankan rute portal billing dengan validasi sesi yang ketat dan perlindungan CSRF. Jangan gunakan cache untuk state billing yang bergantung pada sesi pengguna.
- - [ ] Semua panggilan API billing menggunakan kode sisi server saja.
- - [ ] Batas plan diterapkan pada setiap rute yang dilindungi.
- - [ ] Alur dunning pembayaran gagal dikonfigurasi.
+ ---
+ name: saas-billing
+ description: "Implement and audit SaaS billing systems, subscription state machines, secure webhooks, and local database synchronization / Implementasi dan audit sistem billing SaaS, state machine langganan, webhook aman, dan sinkronisasi database lokal."
+ author: "Roedy Rustam"
+ version: "3.0.0"
+ ---
+
+ # SaaS Billing Expert (2026 Edition)
+
+ [English](#english) | [Bahasa Indonesia](#bahasa-indonesia)
+
+ ---
+
+ <a name="english"></a>
+ ## English
+
+ ### Orchestration & Integration
+ Connects and orchestrates with relevant domain skills like `brainstorming`, `zero-to-prod-orchestrator`, and `session-memory-manager` to ensure cohesive execution.
+
+ ### Description
+ Expert guide for implementing and auditing SaaS billing systems. Covers subscription state machines, secure webhook handling, database synchronization, and the 2026 billing landscape including Stripe, **Polar.sh** (open-source, developer-first), **LemonSqueezy**, PayPal, and Midtrans (for Southeast Asia).
+
+ ### Trigger Conditions
+ - Integrating any payment gateway (Stripe, Polar.sh, LemonSqueezy, Midtrans, PayPal) into a SaaS application.
+ - Using a Static-to-Dynamic QRIS alternative (with unique nominals and mutation webhooks) for local developers without PG accounts.
+ - Implementing subscription state machines (active → past_due → canceled → reactivated).
+ - Building secure webhook handlers with signature verification and idempotency.
+ - Syncing external subscription status to a local database.
+ - Implementing usage-based billing or metered API pricing.
+ - Building the customer billing portal (manage subscription, download invoices).
+ - Auditing an existing billing system for security gaps.
+
+ ### 2026 Billing Provider Landscape
+
+ | Provider | Best For | Open Source | Merchant of Record |
+ |---|---|---|---|
+ | **Stripe** | Enterprise, global, complex billing | ❌ | ❌ |
+ | **Polar.sh** | Developer-first, open-source products | ✅ | ✅ (optional) |
+ | **LemonSqueezy** | Indie hackers, simple pricing, global | ❌ | ✅ |
+ | **Paddle** | B2B SaaS, EU VAT compliance | ❌ | ✅ |
+ | **Midtrans** | Southeast Asia / Indonesia | ❌ | ❌ |
+ | **PayPal** | Global, consumer trust | ❌ | ❌ |
+
+ > **Merchant of Record (MoR)**: The provider handles tax compliance (VAT, GST), chargebacks, and legal liability — ideal for small teams without a finance department.
+
+ ### Polar.sh — Developer-First Billing (2026 Rising Star)
+ Polar.sh is the modern, open-source alternative to Gumroad/LemonSqueezy, purpose-built for developers and open-source projects:
+ ```typescript
+ import { Polar } from "@polar-sh/sdk";
+
+ const polar = new Polar({ accessToken: process.env.POLAR_ACCESS_TOKEN });
+
+ // Create a checkout session
+ const checkout = await polar.checkouts.custom.create({
+ productId: "prod_xxxx",
+ successUrl: "https://myapp.com/success?checkout={CHECKOUT_ID}",
+ customerEmail: user.email,
+ metadata: { userId: user.id },
+ });
+
+ // Redirect to checkout
+ return redirect(checkout.url);
+ ```
+
+ ```typescript
+ // Webhook handler (Next.js App Router)
+ import { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks";
+
+ export async function POST(req: Request) {
+ const body = await req.text();
+ const signature = req.headers.get("webhook-signature") ?? "";
+
+ try {
+ const event = validateEvent(body, req.headers, process.env.POLAR_WEBHOOK_SECRET!);
+
+ switch (event.type) {
+ case "subscription.created":
+ case "subscription.updated":
+ await syncSubscription(event.data);
+ break;
+ case "subscription.canceled":
+ await cancelSubscription(event.data.id);
+ break;
+ }
+ return new Response(null, { status: 200 });
+ } catch (e) {
+ if (e instanceof WebhookVerificationError) {
+ return new Response("Invalid signature", { status: 403 });
+ }
+ throw e;
+ }
+ }
+ ```
+
+ ### Stripe — Production Patterns
+
+ #### Subscription State Machine
+ ```
+ FREE ──subscribe──> TRIALING ──trial_ends──> ACTIVE
+ │
+ ┌───────────────┤
+ │ │
+ payment fails cancel
+ │ │
+ PAST_DUE CANCELED
+ │
+ 3 failed retries
+ │
+ CANCELED
+ ```
+
+ #### Idempotent Webhook Handler
+ ```typescript
+ // app/api/webhooks/stripe/route.ts
+ import Stripe from 'stripe';
+ import { db } from '@/lib/db';
+
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
+
+ export async function POST(req: Request) {
+ const body = await req.text();
+ const sig = req.headers.get('stripe-signature')!;
+
+ let event: Stripe.Event;
+ try {
+ event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
+ } catch {
+ return new Response('Invalid signature', { status: 400 });
+ }
+
+ // Idempotency: skip already-processed events
+ const processed = await db.webhookEvent.findUnique({ where: { stripeEventId: event.id } });
+ if (processed) return new Response(null, { status: 200 });
+
+ // Process event
+ switch (event.type) {
+ case 'customer.subscription.created':
+ case 'customer.subscription.updated': {
+ const sub = event.data.object as Stripe.Subscription;
+ await db.subscription.upsert({
+ where: { stripeSubId: sub.id },
+ create: { stripeSubId: sub.id, status: sub.status, userId: sub.metadata.userId },
+ update: { status: sub.status, currentPeriodEnd: new Date(sub.current_period_end * 1000) },
+ });
+ break;
+ }
+ case 'invoice.payment_failed': {
+ const invoice = event.data.object as Stripe.Invoice;
+ await sendDunningEmail(invoice.customer_email!);
+ break;
+ }
+ }
+
+ // Mark as processed
+ await db.webhookEvent.create({ data: { stripeEventId: event.id } });
+ return new Response(null, { status: 200 });
+ }
+ ```
+
+ #### Usage-Based Billing (Metered)
+ ```typescript
+ // Report usage at end of billing period
+ await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
+ quantity: apiCallsThisMonth,
+ timestamp: Math.floor(Date.now() / 1000),
+ action: 'set', // 'set' or 'increment'
+ });
+ ```
+
+ ### PayPal — Checkout Integration
+
+ PayPal remains a trusted global standard for one-off payments and subscriptions.
+
+ #### Create Order (Server-Side)
+ ```typescript
+ // app/api/paypal/create-order/route.ts
+ import { paypalClient } from '@/lib/paypal';
+ import paypal from '@paypal/checkout-server-sdk';
+
+ export async function POST() {
+ const request = new paypal.orders.OrdersCreateRequest();
+ request.prefer("return=representation");
+ request.requestBody({
+ intent: 'CAPTURE',
+ purchase_units: [{ amount: { currency_code: 'USD', value: '29.99' } }]
+ });
+
+ const response = await paypalClient().execute(request);
+ return Response.json({ id: response.result.id });
+ }
+ ```
+
+ #### Capture Payment (Server-Side)
+ ```typescript
+ // app/api/paypal/capture-order/route.ts
+ import { db } from '@/lib/db';
+ import { paypalClient } from '@/lib/paypal';
+ import paypal from '@paypal/checkout-server-sdk';
+
+ export async function POST(req: Request) {
+ const { orderID, userId } = await req.json();
+ const request = new paypal.orders.OrdersCaptureRequest(orderID);
+ request.requestBody({});
+
+ const response = await paypalClient().execute(request);
+ if (response.result.status === 'COMPLETED') {
+ // Grant access or update subscription in DB
+ await db.subscription.create({
+ data: { userId, provider: 'paypal', status: 'active' }
+ });
+ return Response.json({ success: true });
+ }
+ return Response.json({ success: false }, { status: 400 });
+ }
+ ```
+
+ ### QRIS Static-to-Dynamic (Indonesia Alternative)
+
+ For Indonesian developers without an official Payment Gateway account, you can create a "Dynamic" QRIS experience using a single Static QRIS combined with unique payment amounts and a bank mutation checking service (e.g., Moota, Cekmutasi) via webhook.
+
+ #### 1. Generate Unique Amount (Server-Side)
+ ```typescript
+ // Add a unique 3-digit code to the base price
+ export async function createQrisTransaction(userId: string, basePrice: number) {
+ // Generate a random code between 1 and 999
+ const uniqueCode = Math.floor(Math.random() * 999) + 1;
+ const totalAmount = basePrice + uniqueCode;
+
+ const transaction = await db.transaction.create({
+ data: {
+ userId, basePrice, uniqueCode, totalAmount,
+ status: 'pending', provider: 'qris_static',
+ expiresAt: new Date(Date.now() + 15 * 60 * 1000) // 15 mins expiry
+ }
+ });
+
+ return {
+ transactionId: transaction.id,
+ totalAmount,
+ qrisUrl: "https://myapp.com/static-qris.png" // User must manually input totalAmount
+ };
+ }
+ ```
+
+ #### 2. Mutation Webhook Handler & Real-Time Notification
+ ```typescript
+ // app/api/webhooks/mutation/route.ts
+ import { db } from '@/lib/db';
+ import { pusherServer } from '@/lib/pusher';
+
+ export async function POST(req: Request) {
+ const signature = req.headers.get("signature");
+ // TODO: Verify signature from mutation service (e.g., Moota)
+
+ const mutations = await req.json();
+
+ for (const mutation of mutations) {
+ if (mutation.type === 'CR' && mutation.amount > 0) {
+ // Find pending transaction matching the exact unique amount
+ const tx = await db.transaction.findFirst({
+ where: {
+ totalAmount: mutation.amount,
+ status: 'pending',
+ provider: 'qris_static',
+ expiresAt: { gt: new Date() }
+ }
+ });
+
+ if (tx) {
+ // Mark as paid and activate subscription
+ await db.transaction.update({ where: { id: tx.id }, data: { status: 'paid' } });
+ await db.subscription.create({
+ data: { userId: tx.userId, provider: 'qris_static', status: 'active' }
+ });
+
+ // Trigger real-time notification to frontend
+ await pusherServer.trigger(`payment-${tx.id}`, 'payment-success', { success: true });
+ }
+ }
+ }
+
+ return new Response("OK", { status: 200 });
+ }
+ ```
+
+ ### Database Schema for Multi-Provider Billing
+ ```typescript
+ // Drizzle ORM — supports Stripe, Polar, LemonSqueezy, PayPal, QRIS Static
+ export const subscriptions = pgTable('subscriptions', {
+ id: text('id').primaryKey(),
+ workspaceId: text('workspace_id').references(() => workspaces.id).notNull(),
+ provider: text('provider').$type<'stripe' | 'polar' | 'lemonsqueezy' | 'paypal' | 'qris_static'>().notNull(),
+ externalCustomerId: text('external_customer_id').notNull(),
+ externalSubId: text('external_sub_id').notNull().unique(),
+ status: text('status').$type<'active' | 'trialing' | 'past_due' | 'canceled' | 'paused'>().notNull(),
+ plan: text('plan').$type<'free' | 'pro' | 'enterprise'>().default('free').notNull(),
+ currentPeriodEnd: timestamp('current_period_end'),
+ cancelAtPeriodEnd: boolean('cancel_at_period_end').default(false),
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
+ });
+ ```
+
+ ### Billing Security Checklist
+ - [ ] Webhook signature verified on every request — reject without valid signature.
+ - [ ] Webhook idempotency implemented — never process the same event twice.
+ - [ ] Session Management Optimization: Secure the billing portal route with strict session validation and CSRF protection. Do not cache session-dependent billing states.
+ - [ ] Use Stripe CLI / Polar.sh test webhooks for local development.
+ - [ ] All billing API calls use server-side code only — never expose secret keys to frontend.
+ - [ ] Plan limits enforced on every protected route (not just at checkout).
+ - [ ] Failed payment dunning flow configured (email sequence, grace period).
+ - [ ] Customer portal link available from within the app.
+
+ ---
+
+ <a name="bahasa-indonesia"></a>
+ ## Bahasa Indonesia
+
+ ### Integrasi Orkestrasi
+ Terhubung dan mengorkestrasi skill domain yang relevan seperti `brainstorming`, `zero-to-prod-orchestrator`, dan `session-memory-manager` untuk memastikan eksekusi yang kohesif.
+
+ ### Deskripsi
+ Panduan ahli untuk mengimplementasikan dan mengaudit sistem billing SaaS. Mencakup state machine langganan, penanganan webhook aman, sinkronisasi database, dan lanskap billing 2026 termasuk Stripe, **Polar.sh** (open-source, developer-first), **LemonSqueezy**, PayPal, dan Midtrans (untuk Asia Tenggara).
+
+ ### Kondisi Pemicu
+ - Mengintegrasikan payment gateway (Stripe, Polar.sh, LemonSqueezy, Midtrans, PayPal) ke aplikasi SaaS.
+ - Menggunakan alternatif QRIS Statis menjadi Dinamis (dengan nominal unik dan webhook mutasi) untuk developer lokal tanpa akun PG.
+ - Mengimplementasikan state machine langganan.
+ - Membangun webhook handler aman dengan verifikasi tanda tangan dan idempotency.
+ - Menyinkronkan status langganan eksternal ke database lokal.
+ - Mengimplementasikan billing berbasis penggunaan (metered pricing).
+ - Membangun portal billing pelanggan.
+ - Mengaudit sistem billing yang ada untuk celah keamanan.
+
+ ### Lanskap Provider Billing 2026
+
+ | Provider | Terbaik Untuk | Open Source | Merchant of Record |
+ |---|---|---|---|
+ | **Stripe** | Enterprise, global, billing kompleks | ❌ | ❌ |
+ | **Polar.sh** | Developer-first, produk open-source | ✅ | ✅ (opsional) |
+ | **LemonSqueezy** | Indie hackers, harga sederhana | ❌ | ✅ |
+ | **Paddle** | B2B SaaS, kepatuhan PPN EU | ❌ | ✅ |
+ | **Midtrans** | Asia Tenggara / Indonesia | ❌ | ❌ |
+ | **PayPal** | Global, kepercayaan konsumen (consumer trust) | ❌ | ❌ |
+
+ > **Merchant of Record (MoR)**: Provider menangani kepatuhan pajak (PPN, GST), chargeback, dan tanggung jawab hukum — ideal untuk tim kecil tanpa departemen keuangan.
+
+ ### Polar.sh — Billing Developer-First
+ Polar.sh adalah alternatif open-source modern untuk Gumroad/LemonSqueezy, dirancang khusus untuk developer dan proyek open-source. Mendukung checkout, webhook, dan manajemen langganan dengan SDK TypeScript yang bersih.
+
+ ### PayPal — Integrasi Checkout
+ PayPal sering digunakan sebagai gateway alternatif atau utama karena tingginya kepercayaan konsumen global.
+ - **Server-Side Checkout**: Gunakan `create-order` dan `capture-order` di backend (menggunakan `@paypal/checkout-server-sdk`) untuk memastikan keamanan dan mencegah manipulasi harga di sisi klien.
+ - **Webhook**: Verifikasi webhook dari PayPal untuk langganan yang diperbarui atau dibatalkan.
+
+ ### Stripe — Pola Produksi
+
+ #### State Machine Langganan
+ Kelola transisi status: `FREE → TRIALING → ACTIVE → PAST_DUE → CANCELED → (reaktivasi)`.
+
+ #### Webhook Handler Idempoten
+ Selalu verifikasi tanda tangan webhook, tandai event sebagai diproses di database untuk mencegah duplikasi.
+
+ #### Billing Berbasis Penggunaan (Metered)
+ Laporkan penggunaan API dengan `stripe.subscriptionItems.createUsageRecord()` di akhir periode billing.
+
+ ### Alternatif Lokal: QRIS Statis Rasa Dinamis (Tanpa Akun Payment Gateway)
+ Bagi pengguna/developer di Indonesia yang belum memiliki Payment Gateway (seperti Midtrans), Anda dapat membuat pengalaman QRIS "Dinamis" menggunakan satu gambar QRIS statis biasa.
+ - **Generate Nominal Unik (Endpoint)**: Tambahkan angka unik (misalnya 3 digit acak) ke harga dasar (contoh: Rp 100.000 menjadi Rp 100.123). Simpan ke database sebagai transaksi `pending` dengan batas waktu kadaluarsa (misal 15 menit). Tampilkan gambar QRIS beserta instruksi transfer sesuai nominal unik.
+ - **Webhook Mutasi Bank**: Gunakan layanan pihak ketiga (seperti Moota, Cekmutasi) yang mengirimkan notifikasi webhook (ke `/api/webhooks/mutation`) setiap kali ada uang masuk.
+ - **Validasi Otomatis**: Saat webhook menerima payload mutasi kredit (`CR`), sistem mencari transaksi `pending` yang jumlahnya sama persis (`totalAmount`). Jika cocok, sistem menandai tagihan sebagai lunas (`paid`) dan mengaktifkan langganan.
+ - **Notifikasi Klien**: Gunakan WebSocket (seperti Pusher atau Socket.io) di backend untuk melakukan *trigger event* "pembayaran berhasil". Di frontend, *listen* ke event tersebut dan tampilkan notifikasi *real-time* kepada pengguna secara instan tanpa perlu me-refresh halaman.
+
+ ### Skema Database Multi-Provider
+ Rancang tabel `subscriptions` yang mendukung beberapa provider (`stripe`, `polar`, `lemonsqueezy`, `paypal`, `qris_static`) dengan kolom `provider` dan ID eksternal yang terpisah.
+
+ ### Checklist Keamanan Billing
+ - [ ] Tanda tangan webhook diverifikasi pada setiap permintaan.
+ - [ ] Idempotency webhook diimplementasikan.
+ - [ ] Optimasi Session Management: Amankan rute portal billing dengan validasi sesi yang ketat dan perlindungan CSRF. Jangan gunakan cache untuk state billing yang bergantung pada sesi pengguna.
+ - [ ] Semua panggilan API billing menggunakan kode sisi server saja.
+ - [ ] Batas plan diterapkan pada setiap rute yang dilindungi.
+ - [ ] Alur dunning pembayaran gagal dikonfigurasi.
- [ ] Tautan portal pelanggan tersedia dari dalam aplikasi.
-