vibes-plug 1.0.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.github/workflows/publish.yml +20 -0
  2. package/AGENTS.md +66 -0
  3. package/BLUEPRINT.md +152 -60
  4. package/CHANGELOG.md +50 -0
  5. package/README.md +346 -194
  6. package/package.json +25 -25
  7. package/plugin.json +1 -1
  8. package/skills/ai-cost-token-optimizer/SKILL.md +52 -0
  9. package/skills/ai-llm-integration-expert/SKILL.md +180 -162
  10. package/skills/api-design-expert/SKILL.md +314 -310
  11. package/skills/app-analyzer-optimizer/SKILL.md +193 -189
  12. package/skills/apple-ecosystem-expert/SKILL.md +142 -0
  13. package/skills/async-queue-temporal-expert/SKILL.md +54 -0
  14. package/skills/authentication-identity-expert/SKILL.md +251 -20
  15. package/skills/auto-doc-updater/SKILL.md +214 -204
  16. package/skills/autonomous-chaos-monkey/SKILL.md +63 -0
  17. package/skills/autonomous-red-teamer/SKILL.md +59 -0
  18. package/skills/autonomous-swarm-director/SKILL.md +69 -0
  19. package/skills/autonomous-tdd-debugger/SKILL.md +65 -0
  20. package/skills/bootstrap-to-modern/SKILL.md +90 -86
  21. package/skills/brainstorming/SKILL.md +373 -353
  22. package/skills/browser-automation-expert/SKILL.md +46 -0
  23. package/skills/ci-cd-devops-architect/SKILL.md +72 -45
  24. package/skills/cloud-hosting-expert/SKILL.md +244 -244
  25. package/skills/coderabbit/SKILL.md +192 -192
  26. package/skills/cron-scheduler-expert/SKILL.md +298 -0
  27. package/skills/data-telemetry-expert/SKILL.md +213 -213
  28. package/skills/database-orm-expert/SKILL.md +294 -294
  29. package/skills/dependency-upgrade-migrator/SKILL.md +295 -0
  30. package/skills/design-system-architect/SKILL.md +27 -10
  31. package/skills/doku-mcp-server/SKILL.md +251 -0
  32. package/skills/doku-payment-gateway/SKILL.md +227 -0
  33. package/skills/e2e-testing-expert/SKILL.md +315 -315
  34. package/skills/edge-serverless-db-expert/SKILL.md +43 -0
  35. package/skills/email-notification-expert/SKILL.md +362 -0
  36. package/skills/error-resilience-expert/SKILL.md +480 -0
  37. package/skills/event-driven-architect/SKILL.md +81 -81
  38. package/skills/feature-flag-analytics-expert/SKILL.md +46 -0
  39. package/skills/file-upload-media-expert/SKILL.md +431 -0
  40. package/skills/form-validation-expert/SKILL.md +401 -0
  41. package/skills/fullstack-expert/SKILL.md +202 -202
  42. package/skills/fullstack-expert/references/api_design_guide.md +466 -466
  43. package/skills/fullstack-expert/references/multi_language_backend.md +528 -528
  44. package/skills/fullstack-expert/scripts/api_contract_validator.py +253 -253
  45. package/skills/fullstack-expert/scripts/architecture_analyzer.py +326 -326
  46. package/skills/gemini-agent-booster/SKILL.md +135 -135
  47. package/skills/global-a11y-i18n-expert/SKILL.md +81 -81
  48. package/skills/glsl-shader-expert/SKILL.md +101 -0
  49. package/skills/go-programming-expert/SKILL.md +295 -295
  50. package/skills/graphql-apollo-expert/SKILL.md +108 -0
  51. package/skills/hig/SKILL.md +188 -188
  52. package/skills/hyper-context-synthesizer/SKILL.md +55 -0
  53. package/skills/js-backend-expert/SKILL.md +34 -9
  54. package/skills/legacy-code-translator/SKILL.md +65 -0
  55. package/skills/llm-cost-arbitrage-router/SKILL.md +59 -0
  56. package/skills/logging-error-tracking-expert/SKILL.md +338 -0
  57. package/skills/mcp-client-orchestrator/SKILL.md +70 -0
  58. package/skills/mcp-server-architect/SKILL.md +194 -194
  59. package/skills/micro-frontend-architect/SKILL.md +106 -0
  60. package/skills/mobile-expo-expert/SKILL.md +186 -186
  61. package/skills/mobile-push-notification-expert/SKILL.md +51 -0
  62. package/skills/monday-design-aesthetic/SKILL.md +67 -67
  63. package/skills/monorepo-architect/SKILL.md +227 -227
  64. package/skills/mpa-orchestrator/SKILL.md +101 -101
  65. package/skills/multi-agent-orchestration/SKILL.md +234 -234
  66. package/skills/multiple-entry-points/SKILL.md +55 -55
  67. package/skills/mvc-expert/SKILL.md +231 -231
  68. package/skills/payment-gateway-expert/SKILL.md +45 -45
  69. package/skills/performance-web-vitals/SKILL.md +332 -332
  70. package/skills/post-quantum-crypto-migrator/SKILL.md +57 -0
  71. package/skills/prd-architect/SKILL.md +201 -191
  72. package/skills/proactive-background-watcher/SKILL.md +62 -0
  73. package/skills/production-ready-hardener/PRODUCTION_READINESS_REPORT.md +67 -0
  74. package/skills/production-ready-hardener/SKILL.md +173 -186
  75. package/skills/production-ready-hardener/references/production_checklist.md +161 -161
  76. package/skills/production-ready-hardener/scripts/production_readiness_scanner.py +881 -875
  77. package/skills/project-context-mapper/SKILL.md +79 -0
  78. package/skills/python-programming-expert/SKILL.md +263 -132
  79. package/skills/rate-limit-abuse-prevention/SKILL.md +371 -0
  80. package/skills/realtime-collaboration-expert/SKILL.md +45 -45
  81. package/skills/rust-programming-expert/SKILL.md +235 -235
  82. package/skills/saas-billing/SKILL.md +377 -377
  83. package/skills/saas-multi-tenant/SKILL.md +251 -237
  84. package/skills/saas-mvp-launcher/SKILL.md +10 -0
  85. package/skills/saas-transformer/SKILL.md +187 -144
  86. package/skills/saas-transformer/references/billing_integration_guide.md +401 -401
  87. package/skills/saas-transformer/references/feature_gating_patterns.md +137 -137
  88. package/skills/saas-transformer/references/saas_transformation_checklist.md +121 -121
  89. package/skills/saas-transformer/scripts/saas_transformation_scanner.py +39 -29
  90. package/skills/scalability-clean-code/SKILL.md +229 -229
  91. package/skills/self-evolving-memory-graph/SKILL.md +75 -0
  92. package/skills/self-healing-cloud-orchestrator/SKILL.md +57 -0
  93. package/skills/senior-frontend/SKILL.md +161 -161
  94. package/skills/senior-fullstack/SKILL.md +167 -167
  95. package/skills/seo/SKILL.md +235 -225
  96. package/skills/seo-geo/SKILL.md +188 -188
  97. package/skills/session-context-loader/SKILL.md +77 -0
  98. package/skills/session-handoff-resume/SKILL.md +158 -158
  99. package/skills/skill_baru/SKILL.md +172 -147
  100. package/skills/spa-orchestrator/SKILL.md +288 -288
  101. package/skills/state-management-expert/SKILL.md +272 -272
  102. package/skills/supabase-security-expert/SKILL.md +243 -243
  103. package/skills/tailwind-expert/SKILL.md +188 -188
  104. package/skills/tanstack-query-expert/SKILL.md +199 -199
  105. package/skills/token-saver/SKILL.md +119 -111
  106. package/skills/typescript-expert/SKILL.md +324 -279
  107. package/skills/ui-components-expert/SKILL.md +263 -46
  108. package/skills/ui-ux-pro-max/SKILL.md +202 -201
  109. package/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-310.pyc +0 -0
  110. package/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-310.pyc +0 -0
  111. package/skills/ui_ux_expert/SKILL.md +17 -6
  112. package/skills/vector-db-rag-expert/SKILL.md +52 -0
  113. package/skills/vibe-code-gardener/SKILL.md +181 -173
  114. package/skills/visual-qa-vision-agent/SKILL.md +65 -0
  115. package/skills/vue-frontend-expert/SKILL.md +126 -0
  116. package/skills/web-3d-graphics-expert/SKILL.md +131 -0
  117. package/skills/web-game-engine-expert/SKILL.md +96 -0
  118. package/skills/web-scraper/SKILL.md +207 -205
  119. package/skills/website-design-cloner/SKILL.md +174 -0
  120. package/skills/webxr-ar-vr-expert/SKILL.md +117 -0
  121. package/skills/zero-to-prod-orchestrator/SKILL.md +206 -180
  122. package/skills/zero-trust-secret-vault/SKILL.md +40 -0
  123. package/vibes-swarm-demo.gif +0 -0
@@ -1,377 +1,377 @@
1
- ---
2
- name: saas-billing
3
- 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."
4
- author: "Roedy Rustam"
5
- ---
6
-
7
- # SaaS Billing Expert (2026 Edition)
8
-
9
- [English](#english) | [Bahasa Indonesia](#bahasa-indonesia)
10
-
11
- ---
12
-
13
- <a name="english"></a>
14
- ## English
15
-
16
- ### Description
17
- 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).
18
-
19
- ### Trigger Conditions
20
- - Integrating any payment gateway (Stripe, Polar.sh, LemonSqueezy, Midtrans, PayPal) into a SaaS application.
21
- - Using a Static-to-Dynamic QRIS alternative (with unique nominals and mutation webhooks) for local developers without PG accounts.
22
- - Implementing subscription state machines (active → past_due → canceled → reactivated).
23
- - Building secure webhook handlers with signature verification and idempotency.
24
- - Syncing external subscription status to a local database.
25
- - Implementing usage-based billing or metered API pricing.
26
- - Building the customer billing portal (manage subscription, download invoices).
27
- - Auditing an existing billing system for security gaps.
28
-
29
- ### 2026 Billing Provider Landscape
30
-
31
- | Provider | Best For | Open Source | Merchant of Record |
32
- |---|---|---|---|
33
- | **Stripe** | Enterprise, global, complex billing | ❌ | ❌ |
34
- | **Polar.sh** | Developer-first, open-source products | ✅ | ✅ (optional) |
35
- | **LemonSqueezy** | Indie hackers, simple pricing, global | ❌ | ✅ |
36
- | **Paddle** | B2B SaaS, EU VAT compliance | ❌ | ✅ |
37
- | **Midtrans** | Southeast Asia / Indonesia | ❌ | ❌ |
38
- | **PayPal** | Global, consumer trust | ❌ | ❌ |
39
-
40
- > **Merchant of Record (MoR)**: The provider handles tax compliance (VAT, GST), chargebacks, and legal liability — ideal for small teams without a finance department.
41
-
42
- ### Polar.sh — Developer-First Billing (2026 Rising Star)
43
- Polar.sh is the modern, open-source alternative to Gumroad/LemonSqueezy, purpose-built for developers and open-source projects:
44
- ```typescript
45
- import { Polar } from "@polar-sh/sdk";
46
-
47
- const polar = new Polar({ accessToken: process.env.POLAR_ACCESS_TOKEN });
48
-
49
- // Create a checkout session
50
- const checkout = await polar.checkouts.custom.create({
51
- productId: "prod_xxxx",
52
- successUrl: "https://myapp.com/success?checkout={CHECKOUT_ID}",
53
- customerEmail: user.email,
54
- metadata: { userId: user.id },
55
- });
56
-
57
- // Redirect to checkout
58
- return redirect(checkout.url);
59
- ```
60
-
61
- ```typescript
62
- // Webhook handler (Next.js App Router)
63
- import { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks";
64
-
65
- export async function POST(req: Request) {
66
- const body = await req.text();
67
- const signature = req.headers.get("webhook-signature") ?? "";
68
-
69
- try {
70
- const event = validateEvent(body, req.headers, process.env.POLAR_WEBHOOK_SECRET!);
71
-
72
- switch (event.type) {
73
- case "subscription.created":
74
- case "subscription.updated":
75
- await syncSubscription(event.data);
76
- break;
77
- case "subscription.canceled":
78
- await cancelSubscription(event.data.id);
79
- break;
80
- }
81
- return new Response(null, { status: 200 });
82
- } catch (e) {
83
- if (e instanceof WebhookVerificationError) {
84
- return new Response("Invalid signature", { status: 403 });
85
- }
86
- throw e;
87
- }
88
- }
89
- ```
90
-
91
- ### Stripe — Production Patterns
92
-
93
- #### Subscription State Machine
94
- ```
95
- FREE ──subscribe──> TRIALING ──trial_ends──> ACTIVE
96
-
97
- ┌───────────────┤
98
- │ │
99
- payment fails cancel
100
- │ │
101
- PAST_DUE CANCELED
102
-
103
- 3 failed retries
104
-
105
- CANCELED
106
- ```
107
-
108
- #### Idempotent Webhook Handler
109
- ```typescript
110
- // app/api/webhooks/stripe/route.ts
111
- import Stripe from 'stripe';
112
- import { db } from '@/lib/db';
113
-
114
- const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
115
-
116
- export async function POST(req: Request) {
117
- const body = await req.text();
118
- const sig = req.headers.get('stripe-signature')!;
119
-
120
- let event: Stripe.Event;
121
- try {
122
- event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
123
- } catch {
124
- return new Response('Invalid signature', { status: 400 });
125
- }
126
-
127
- // Idempotency: skip already-processed events
128
- const processed = await db.webhookEvent.findUnique({ where: { stripeEventId: event.id } });
129
- if (processed) return new Response(null, { status: 200 });
130
-
131
- // Process event
132
- switch (event.type) {
133
- case 'customer.subscription.created':
134
- case 'customer.subscription.updated': {
135
- const sub = event.data.object as Stripe.Subscription;
136
- await db.subscription.upsert({
137
- where: { stripeSubId: sub.id },
138
- create: { stripeSubId: sub.id, status: sub.status, userId: sub.metadata.userId },
139
- update: { status: sub.status, currentPeriodEnd: new Date(sub.current_period_end * 1000) },
140
- });
141
- break;
142
- }
143
- case 'invoice.payment_failed': {
144
- const invoice = event.data.object as Stripe.Invoice;
145
- await sendDunningEmail(invoice.customer_email!);
146
- break;
147
- }
148
- }
149
-
150
- // Mark as processed
151
- await db.webhookEvent.create({ data: { stripeEventId: event.id } });
152
- return new Response(null, { status: 200 });
153
- }
154
- ```
155
-
156
- #### Usage-Based Billing (Metered)
157
- ```typescript
158
- // Report usage at end of billing period
159
- await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
160
- quantity: apiCallsThisMonth,
161
- timestamp: Math.floor(Date.now() / 1000),
162
- action: 'set', // 'set' or 'increment'
163
- });
164
- ```
165
-
166
- ### PayPal — Checkout Integration
167
-
168
- PayPal remains a trusted global standard for one-off payments and subscriptions.
169
-
170
- #### Create Order (Server-Side)
171
- ```typescript
172
- // app/api/paypal/create-order/route.ts
173
- import { paypalClient } from '@/lib/paypal';
174
- import paypal from '@paypal/checkout-server-sdk';
175
-
176
- export async function POST() {
177
- const request = new paypal.orders.OrdersCreateRequest();
178
- request.prefer("return=representation");
179
- request.requestBody({
180
- intent: 'CAPTURE',
181
- purchase_units: [{ amount: { currency_code: 'USD', value: '29.99' } }]
182
- });
183
-
184
- const response = await paypalClient().execute(request);
185
- return Response.json({ id: response.result.id });
186
- }
187
- ```
188
-
189
- #### Capture Payment (Server-Side)
190
- ```typescript
191
- // app/api/paypal/capture-order/route.ts
192
- import { db } from '@/lib/db';
193
- import { paypalClient } from '@/lib/paypal';
194
- import paypal from '@paypal/checkout-server-sdk';
195
-
196
- export async function POST(req: Request) {
197
- const { orderID, userId } = await req.json();
198
- const request = new paypal.orders.OrdersCaptureRequest(orderID);
199
- request.requestBody({});
200
-
201
- const response = await paypalClient().execute(request);
202
- if (response.result.status === 'COMPLETED') {
203
- // Grant access or update subscription in DB
204
- await db.subscription.create({
205
- data: { userId, provider: 'paypal', status: 'active' }
206
- });
207
- return Response.json({ success: true });
208
- }
209
- return Response.json({ success: false }, { status: 400 });
210
- }
211
- ```
212
-
213
- ### QRIS Static-to-Dynamic (Indonesia Alternative)
214
-
215
- 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.
216
-
217
- #### 1. Generate Unique Amount (Server-Side)
218
- ```typescript
219
- // Add a unique 3-digit code to the base price
220
- export async function createQrisTransaction(userId: string, basePrice: number) {
221
- // Generate a random code between 1 and 999
222
- const uniqueCode = Math.floor(Math.random() * 999) + 1;
223
- const totalAmount = basePrice + uniqueCode;
224
-
225
- const transaction = await db.transaction.create({
226
- data: {
227
- userId, basePrice, uniqueCode, totalAmount,
228
- status: 'pending', provider: 'qris_static',
229
- expiresAt: new Date(Date.now() + 15 * 60 * 1000) // 15 mins expiry
230
- }
231
- });
232
-
233
- return {
234
- transactionId: transaction.id,
235
- totalAmount,
236
- qrisUrl: "https://myapp.com/static-qris.png" // User must manually input totalAmount
237
- };
238
- }
239
- ```
240
-
241
- #### 2. Mutation Webhook Handler & Real-Time Notification
242
- ```typescript
243
- // app/api/webhooks/mutation/route.ts
244
- import { db } from '@/lib/db';
245
- import { pusherServer } from '@/lib/pusher';
246
-
247
- export async function POST(req: Request) {
248
- const signature = req.headers.get("signature");
249
- // TODO: Verify signature from mutation service (e.g., Moota)
250
-
251
- const mutations = await req.json();
252
-
253
- for (const mutation of mutations) {
254
- if (mutation.type === 'CR' && mutation.amount > 0) {
255
- // Find pending transaction matching the exact unique amount
256
- const tx = await db.transaction.findFirst({
257
- where: {
258
- totalAmount: mutation.amount,
259
- status: 'pending',
260
- provider: 'qris_static',
261
- expiresAt: { gt: new Date() }
262
- }
263
- });
264
-
265
- if (tx) {
266
- // Mark as paid and activate subscription
267
- await db.transaction.update({ where: { id: tx.id }, data: { status: 'paid' } });
268
- await db.subscription.create({
269
- data: { userId: tx.userId, provider: 'qris_static', status: 'active' }
270
- });
271
-
272
- // Trigger real-time notification to frontend
273
- await pusherServer.trigger(`payment-${tx.id}`, 'payment-success', { success: true });
274
- }
275
- }
276
- }
277
-
278
- return new Response("OK", { status: 200 });
279
- }
280
- ```
281
-
282
- ### Database Schema for Multi-Provider Billing
283
- ```typescript
284
- // Drizzle ORM — supports Stripe, Polar, LemonSqueezy, PayPal, QRIS Static
285
- export const subscriptions = pgTable('subscriptions', {
286
- id: text('id').primaryKey(),
287
- workspaceId: text('workspace_id').references(() => workspaces.id).notNull(),
288
- provider: text('provider').$type<'stripe' | 'polar' | 'lemonsqueezy' | 'paypal' | 'qris_static'>().notNull(),
289
- externalCustomerId: text('external_customer_id').notNull(),
290
- externalSubId: text('external_sub_id').notNull().unique(),
291
- status: text('status').$type<'active' | 'trialing' | 'past_due' | 'canceled' | 'paused'>().notNull(),
292
- plan: text('plan').$type<'free' | 'pro' | 'enterprise'>().default('free').notNull(),
293
- currentPeriodEnd: timestamp('current_period_end'),
294
- cancelAtPeriodEnd: boolean('cancel_at_period_end').default(false),
295
- createdAt: timestamp('created_at').defaultNow().notNull(),
296
- updatedAt: timestamp('updated_at').defaultNow().notNull(),
297
- });
298
- ```
299
-
300
- ### Billing Security Checklist
301
- - [ ] Webhook signature verified on every request — reject without valid signature.
302
- - [ ] Webhook idempotency implemented — never process the same event twice.
303
- - [ ] Session Management Optimization: Secure the billing portal route with strict session validation and CSRF protection. Do not cache session-dependent billing states.
304
- - [ ] Use Stripe CLI / Polar.sh test webhooks for local development.
305
- - [ ] All billing API calls use server-side code only — never expose secret keys to frontend.
306
- - [ ] Plan limits enforced on every protected route (not just at checkout).
307
- - [ ] Failed payment dunning flow configured (email sequence, grace period).
308
- - [ ] Customer portal link available from within the app.
309
-
310
- ---
311
-
312
- <a name="bahasa-indonesia"></a>
313
- ## Bahasa Indonesia
314
-
315
- ### Deskripsi
316
- 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).
317
-
318
- ### Kondisi Pemicu
319
- - Mengintegrasikan payment gateway (Stripe, Polar.sh, LemonSqueezy, Midtrans, PayPal) ke aplikasi SaaS.
320
- - Menggunakan alternatif QRIS Statis menjadi Dinamis (dengan nominal unik dan webhook mutasi) untuk developer lokal tanpa akun PG.
321
- - Mengimplementasikan state machine langganan.
322
- - Membangun webhook handler aman dengan verifikasi tanda tangan dan idempotency.
323
- - Menyinkronkan status langganan eksternal ke database lokal.
324
- - Mengimplementasikan billing berbasis penggunaan (metered pricing).
325
- - Membangun portal billing pelanggan.
326
- - Mengaudit sistem billing yang ada untuk celah keamanan.
327
-
328
- ### Lanskap Provider Billing 2026
329
-
330
- | Provider | Terbaik Untuk | Open Source | Merchant of Record |
331
- |---|---|---|---|
332
- | **Stripe** | Enterprise, global, billing kompleks | ❌ | ❌ |
333
- | **Polar.sh** | Developer-first, produk open-source | ✅ | ✅ (opsional) |
334
- | **LemonSqueezy** | Indie hackers, harga sederhana | ❌ | ✅ |
335
- | **Paddle** | B2B SaaS, kepatuhan PPN EU | ❌ | ✅ |
336
- | **Midtrans** | Asia Tenggara / Indonesia | ❌ | ❌ |
337
- | **PayPal** | Global, kepercayaan konsumen (consumer trust) | ❌ | ❌ |
338
-
339
- > **Merchant of Record (MoR)**: Provider menangani kepatuhan pajak (PPN, GST), chargeback, dan tanggung jawab hukum — ideal untuk tim kecil tanpa departemen keuangan.
340
-
341
- ### Polar.sh — Billing Developer-First
342
- 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.
343
-
344
- ### PayPal — Integrasi Checkout
345
- PayPal sering digunakan sebagai gateway alternatif atau utama karena tingginya kepercayaan konsumen global.
346
- - **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.
347
- - **Webhook**: Verifikasi webhook dari PayPal untuk langganan yang diperbarui atau dibatalkan.
348
-
349
- ### Stripe — Pola Produksi
350
-
351
- #### State Machine Langganan
352
- Kelola transisi status: `FREE → TRIALING → ACTIVE → PAST_DUE → CANCELED → (reaktivasi)`.
353
-
354
- #### Webhook Handler Idempoten
355
- Selalu verifikasi tanda tangan webhook, tandai event sebagai diproses di database untuk mencegah duplikasi.
356
-
357
- #### Billing Berbasis Penggunaan (Metered)
358
- Laporkan penggunaan API dengan `stripe.subscriptionItems.createUsageRecord()` di akhir periode billing.
359
-
360
- ### Alternatif Lokal: QRIS Statis Rasa Dinamis (Tanpa Akun Payment Gateway)
361
- Bagi pengguna/developer di Indonesia yang belum memiliki Payment Gateway (seperti Midtrans), Anda dapat membuat pengalaman QRIS "Dinamis" menggunakan satu gambar QRIS statis biasa.
362
- - **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.
363
- - **Webhook Mutasi Bank**: Gunakan layanan pihak ketiga (seperti Moota, Cekmutasi) yang mengirimkan notifikasi webhook (ke `/api/webhooks/mutation`) setiap kali ada uang masuk.
364
- - **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.
365
- - **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.
366
-
367
- ### Skema Database Multi-Provider
368
- Rancang tabel `subscriptions` yang mendukung beberapa provider (`stripe`, `polar`, `lemonsqueezy`, `paypal`, `qris_static`) dengan kolom `provider` dan ID eksternal yang terpisah.
369
-
370
- ### Checklist Keamanan Billing
371
- - [ ] Tanda tangan webhook diverifikasi pada setiap permintaan.
372
- - [ ] Idempotency webhook diimplementasikan.
373
- - [ ] 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.
374
- - [ ] Semua panggilan API billing menggunakan kode sisi server saja.
375
- - [ ] Batas plan diterapkan pada setiap rute yang dilindungi.
376
- - [ ] Alur dunning pembayaran gagal dikonfigurasi.
377
- - [ ] Tautan portal pelanggan tersedia dari dalam aplikasi.
1
+ ---
2
+ name: saas-billing
3
+ 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."
4
+ author: "Roedy Rustam"
5
+ ---
6
+
7
+ # SaaS Billing Expert (2026 Edition)
8
+
9
+ [English](#english) | [Bahasa Indonesia](#bahasa-indonesia)
10
+
11
+ ---
12
+
13
+ <a name="english"></a>
14
+ ## English
15
+
16
+ ### Description
17
+ 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).
18
+
19
+ ### Trigger Conditions
20
+ - Integrating any payment gateway (Stripe, Polar.sh, LemonSqueezy, Midtrans, PayPal) into a SaaS application.
21
+ - Using a Static-to-Dynamic QRIS alternative (with unique nominals and mutation webhooks) for local developers without PG accounts.
22
+ - Implementing subscription state machines (active → past_due → canceled → reactivated).
23
+ - Building secure webhook handlers with signature verification and idempotency.
24
+ - Syncing external subscription status to a local database.
25
+ - Implementing usage-based billing or metered API pricing.
26
+ - Building the customer billing portal (manage subscription, download invoices).
27
+ - Auditing an existing billing system for security gaps.
28
+
29
+ ### 2026 Billing Provider Landscape
30
+
31
+ | Provider | Best For | Open Source | Merchant of Record |
32
+ |---|---|---|---|
33
+ | **Stripe** | Enterprise, global, complex billing | ❌ | ❌ |
34
+ | **Polar.sh** | Developer-first, open-source products | ✅ | ✅ (optional) |
35
+ | **LemonSqueezy** | Indie hackers, simple pricing, global | ❌ | ✅ |
36
+ | **Paddle** | B2B SaaS, EU VAT compliance | ❌ | ✅ |
37
+ | **Midtrans** | Southeast Asia / Indonesia | ❌ | ❌ |
38
+ | **PayPal** | Global, consumer trust | ❌ | ❌ |
39
+
40
+ > **Merchant of Record (MoR)**: The provider handles tax compliance (VAT, GST), chargebacks, and legal liability — ideal for small teams without a finance department.
41
+
42
+ ### Polar.sh — Developer-First Billing (2026 Rising Star)
43
+ Polar.sh is the modern, open-source alternative to Gumroad/LemonSqueezy, purpose-built for developers and open-source projects:
44
+ ```typescript
45
+ import { Polar } from "@polar-sh/sdk";
46
+
47
+ const polar = new Polar({ accessToken: process.env.POLAR_ACCESS_TOKEN });
48
+
49
+ // Create a checkout session
50
+ const checkout = await polar.checkouts.custom.create({
51
+ productId: "prod_xxxx",
52
+ successUrl: "https://myapp.com/success?checkout={CHECKOUT_ID}",
53
+ customerEmail: user.email,
54
+ metadata: { userId: user.id },
55
+ });
56
+
57
+ // Redirect to checkout
58
+ return redirect(checkout.url);
59
+ ```
60
+
61
+ ```typescript
62
+ // Webhook handler (Next.js App Router)
63
+ import { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks";
64
+
65
+ export async function POST(req: Request) {
66
+ const body = await req.text();
67
+ const signature = req.headers.get("webhook-signature") ?? "";
68
+
69
+ try {
70
+ const event = validateEvent(body, req.headers, process.env.POLAR_WEBHOOK_SECRET!);
71
+
72
+ switch (event.type) {
73
+ case "subscription.created":
74
+ case "subscription.updated":
75
+ await syncSubscription(event.data);
76
+ break;
77
+ case "subscription.canceled":
78
+ await cancelSubscription(event.data.id);
79
+ break;
80
+ }
81
+ return new Response(null, { status: 200 });
82
+ } catch (e) {
83
+ if (e instanceof WebhookVerificationError) {
84
+ return new Response("Invalid signature", { status: 403 });
85
+ }
86
+ throw e;
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### Stripe — Production Patterns
92
+
93
+ #### Subscription State Machine
94
+ ```
95
+ FREE ──subscribe──> TRIALING ──trial_ends──> ACTIVE
96
+
97
+ ┌───────────────┤
98
+ │ │
99
+ payment fails cancel
100
+ │ │
101
+ PAST_DUE CANCELED
102
+
103
+ 3 failed retries
104
+
105
+ CANCELED
106
+ ```
107
+
108
+ #### Idempotent Webhook Handler
109
+ ```typescript
110
+ // app/api/webhooks/stripe/route.ts
111
+ import Stripe from 'stripe';
112
+ import { db } from '@/lib/db';
113
+
114
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
115
+
116
+ export async function POST(req: Request) {
117
+ const body = await req.text();
118
+ const sig = req.headers.get('stripe-signature')!;
119
+
120
+ let event: Stripe.Event;
121
+ try {
122
+ event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
123
+ } catch {
124
+ return new Response('Invalid signature', { status: 400 });
125
+ }
126
+
127
+ // Idempotency: skip already-processed events
128
+ const processed = await db.webhookEvent.findUnique({ where: { stripeEventId: event.id } });
129
+ if (processed) return new Response(null, { status: 200 });
130
+
131
+ // Process event
132
+ switch (event.type) {
133
+ case 'customer.subscription.created':
134
+ case 'customer.subscription.updated': {
135
+ const sub = event.data.object as Stripe.Subscription;
136
+ await db.subscription.upsert({
137
+ where: { stripeSubId: sub.id },
138
+ create: { stripeSubId: sub.id, status: sub.status, userId: sub.metadata.userId },
139
+ update: { status: sub.status, currentPeriodEnd: new Date(sub.current_period_end * 1000) },
140
+ });
141
+ break;
142
+ }
143
+ case 'invoice.payment_failed': {
144
+ const invoice = event.data.object as Stripe.Invoice;
145
+ await sendDunningEmail(invoice.customer_email!);
146
+ break;
147
+ }
148
+ }
149
+
150
+ // Mark as processed
151
+ await db.webhookEvent.create({ data: { stripeEventId: event.id } });
152
+ return new Response(null, { status: 200 });
153
+ }
154
+ ```
155
+
156
+ #### Usage-Based Billing (Metered)
157
+ ```typescript
158
+ // Report usage at end of billing period
159
+ await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
160
+ quantity: apiCallsThisMonth,
161
+ timestamp: Math.floor(Date.now() / 1000),
162
+ action: 'set', // 'set' or 'increment'
163
+ });
164
+ ```
165
+
166
+ ### PayPal — Checkout Integration
167
+
168
+ PayPal remains a trusted global standard for one-off payments and subscriptions.
169
+
170
+ #### Create Order (Server-Side)
171
+ ```typescript
172
+ // app/api/paypal/create-order/route.ts
173
+ import { paypalClient } from '@/lib/paypal';
174
+ import paypal from '@paypal/checkout-server-sdk';
175
+
176
+ export async function POST() {
177
+ const request = new paypal.orders.OrdersCreateRequest();
178
+ request.prefer("return=representation");
179
+ request.requestBody({
180
+ intent: 'CAPTURE',
181
+ purchase_units: [{ amount: { currency_code: 'USD', value: '29.99' } }]
182
+ });
183
+
184
+ const response = await paypalClient().execute(request);
185
+ return Response.json({ id: response.result.id });
186
+ }
187
+ ```
188
+
189
+ #### Capture Payment (Server-Side)
190
+ ```typescript
191
+ // app/api/paypal/capture-order/route.ts
192
+ import { db } from '@/lib/db';
193
+ import { paypalClient } from '@/lib/paypal';
194
+ import paypal from '@paypal/checkout-server-sdk';
195
+
196
+ export async function POST(req: Request) {
197
+ const { orderID, userId } = await req.json();
198
+ const request = new paypal.orders.OrdersCaptureRequest(orderID);
199
+ request.requestBody({});
200
+
201
+ const response = await paypalClient().execute(request);
202
+ if (response.result.status === 'COMPLETED') {
203
+ // Grant access or update subscription in DB
204
+ await db.subscription.create({
205
+ data: { userId, provider: 'paypal', status: 'active' }
206
+ });
207
+ return Response.json({ success: true });
208
+ }
209
+ return Response.json({ success: false }, { status: 400 });
210
+ }
211
+ ```
212
+
213
+ ### QRIS Static-to-Dynamic (Indonesia Alternative)
214
+
215
+ 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.
216
+
217
+ #### 1. Generate Unique Amount (Server-Side)
218
+ ```typescript
219
+ // Add a unique 3-digit code to the base price
220
+ export async function createQrisTransaction(userId: string, basePrice: number) {
221
+ // Generate a random code between 1 and 999
222
+ const uniqueCode = Math.floor(Math.random() * 999) + 1;
223
+ const totalAmount = basePrice + uniqueCode;
224
+
225
+ const transaction = await db.transaction.create({
226
+ data: {
227
+ userId, basePrice, uniqueCode, totalAmount,
228
+ status: 'pending', provider: 'qris_static',
229
+ expiresAt: new Date(Date.now() + 15 * 60 * 1000) // 15 mins expiry
230
+ }
231
+ });
232
+
233
+ return {
234
+ transactionId: transaction.id,
235
+ totalAmount,
236
+ qrisUrl: "https://myapp.com/static-qris.png" // User must manually input totalAmount
237
+ };
238
+ }
239
+ ```
240
+
241
+ #### 2. Mutation Webhook Handler & Real-Time Notification
242
+ ```typescript
243
+ // app/api/webhooks/mutation/route.ts
244
+ import { db } from '@/lib/db';
245
+ import { pusherServer } from '@/lib/pusher';
246
+
247
+ export async function POST(req: Request) {
248
+ const signature = req.headers.get("signature");
249
+ // TODO: Verify signature from mutation service (e.g., Moota)
250
+
251
+ const mutations = await req.json();
252
+
253
+ for (const mutation of mutations) {
254
+ if (mutation.type === 'CR' && mutation.amount > 0) {
255
+ // Find pending transaction matching the exact unique amount
256
+ const tx = await db.transaction.findFirst({
257
+ where: {
258
+ totalAmount: mutation.amount,
259
+ status: 'pending',
260
+ provider: 'qris_static',
261
+ expiresAt: { gt: new Date() }
262
+ }
263
+ });
264
+
265
+ if (tx) {
266
+ // Mark as paid and activate subscription
267
+ await db.transaction.update({ where: { id: tx.id }, data: { status: 'paid' } });
268
+ await db.subscription.create({
269
+ data: { userId: tx.userId, provider: 'qris_static', status: 'active' }
270
+ });
271
+
272
+ // Trigger real-time notification to frontend
273
+ await pusherServer.trigger(`payment-${tx.id}`, 'payment-success', { success: true });
274
+ }
275
+ }
276
+ }
277
+
278
+ return new Response("OK", { status: 200 });
279
+ }
280
+ ```
281
+
282
+ ### Database Schema for Multi-Provider Billing
283
+ ```typescript
284
+ // Drizzle ORM — supports Stripe, Polar, LemonSqueezy, PayPal, QRIS Static
285
+ export const subscriptions = pgTable('subscriptions', {
286
+ id: text('id').primaryKey(),
287
+ workspaceId: text('workspace_id').references(() => workspaces.id).notNull(),
288
+ provider: text('provider').$type<'stripe' | 'polar' | 'lemonsqueezy' | 'paypal' | 'qris_static'>().notNull(),
289
+ externalCustomerId: text('external_customer_id').notNull(),
290
+ externalSubId: text('external_sub_id').notNull().unique(),
291
+ status: text('status').$type<'active' | 'trialing' | 'past_due' | 'canceled' | 'paused'>().notNull(),
292
+ plan: text('plan').$type<'free' | 'pro' | 'enterprise'>().default('free').notNull(),
293
+ currentPeriodEnd: timestamp('current_period_end'),
294
+ cancelAtPeriodEnd: boolean('cancel_at_period_end').default(false),
295
+ createdAt: timestamp('created_at').defaultNow().notNull(),
296
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
297
+ });
298
+ ```
299
+
300
+ ### Billing Security Checklist
301
+ - [ ] Webhook signature verified on every request — reject without valid signature.
302
+ - [ ] Webhook idempotency implemented — never process the same event twice.
303
+ - [ ] Session Management Optimization: Secure the billing portal route with strict session validation and CSRF protection. Do not cache session-dependent billing states.
304
+ - [ ] Use Stripe CLI / Polar.sh test webhooks for local development.
305
+ - [ ] All billing API calls use server-side code only — never expose secret keys to frontend.
306
+ - [ ] Plan limits enforced on every protected route (not just at checkout).
307
+ - [ ] Failed payment dunning flow configured (email sequence, grace period).
308
+ - [ ] Customer portal link available from within the app.
309
+
310
+ ---
311
+
312
+ <a name="bahasa-indonesia"></a>
313
+ ## Bahasa Indonesia
314
+
315
+ ### Deskripsi
316
+ 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).
317
+
318
+ ### Kondisi Pemicu
319
+ - Mengintegrasikan payment gateway (Stripe, Polar.sh, LemonSqueezy, Midtrans, PayPal) ke aplikasi SaaS.
320
+ - Menggunakan alternatif QRIS Statis menjadi Dinamis (dengan nominal unik dan webhook mutasi) untuk developer lokal tanpa akun PG.
321
+ - Mengimplementasikan state machine langganan.
322
+ - Membangun webhook handler aman dengan verifikasi tanda tangan dan idempotency.
323
+ - Menyinkronkan status langganan eksternal ke database lokal.
324
+ - Mengimplementasikan billing berbasis penggunaan (metered pricing).
325
+ - Membangun portal billing pelanggan.
326
+ - Mengaudit sistem billing yang ada untuk celah keamanan.
327
+
328
+ ### Lanskap Provider Billing 2026
329
+
330
+ | Provider | Terbaik Untuk | Open Source | Merchant of Record |
331
+ |---|---|---|---|
332
+ | **Stripe** | Enterprise, global, billing kompleks | ❌ | ❌ |
333
+ | **Polar.sh** | Developer-first, produk open-source | ✅ | ✅ (opsional) |
334
+ | **LemonSqueezy** | Indie hackers, harga sederhana | ❌ | ✅ |
335
+ | **Paddle** | B2B SaaS, kepatuhan PPN EU | ❌ | ✅ |
336
+ | **Midtrans** | Asia Tenggara / Indonesia | ❌ | ❌ |
337
+ | **PayPal** | Global, kepercayaan konsumen (consumer trust) | ❌ | ❌ |
338
+
339
+ > **Merchant of Record (MoR)**: Provider menangani kepatuhan pajak (PPN, GST), chargeback, dan tanggung jawab hukum — ideal untuk tim kecil tanpa departemen keuangan.
340
+
341
+ ### Polar.sh — Billing Developer-First
342
+ 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.
343
+
344
+ ### PayPal — Integrasi Checkout
345
+ PayPal sering digunakan sebagai gateway alternatif atau utama karena tingginya kepercayaan konsumen global.
346
+ - **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.
347
+ - **Webhook**: Verifikasi webhook dari PayPal untuk langganan yang diperbarui atau dibatalkan.
348
+
349
+ ### Stripe — Pola Produksi
350
+
351
+ #### State Machine Langganan
352
+ Kelola transisi status: `FREE → TRIALING → ACTIVE → PAST_DUE → CANCELED → (reaktivasi)`.
353
+
354
+ #### Webhook Handler Idempoten
355
+ Selalu verifikasi tanda tangan webhook, tandai event sebagai diproses di database untuk mencegah duplikasi.
356
+
357
+ #### Billing Berbasis Penggunaan (Metered)
358
+ Laporkan penggunaan API dengan `stripe.subscriptionItems.createUsageRecord()` di akhir periode billing.
359
+
360
+ ### Alternatif Lokal: QRIS Statis Rasa Dinamis (Tanpa Akun Payment Gateway)
361
+ Bagi pengguna/developer di Indonesia yang belum memiliki Payment Gateway (seperti Midtrans), Anda dapat membuat pengalaman QRIS "Dinamis" menggunakan satu gambar QRIS statis biasa.
362
+ - **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.
363
+ - **Webhook Mutasi Bank**: Gunakan layanan pihak ketiga (seperti Moota, Cekmutasi) yang mengirimkan notifikasi webhook (ke `/api/webhooks/mutation`) setiap kali ada uang masuk.
364
+ - **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.
365
+ - **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.
366
+
367
+ ### Skema Database Multi-Provider
368
+ Rancang tabel `subscriptions` yang mendukung beberapa provider (`stripe`, `polar`, `lemonsqueezy`, `paypal`, `qris_static`) dengan kolom `provider` dan ID eksternal yang terpisah.
369
+
370
+ ### Checklist Keamanan Billing
371
+ - [ ] Tanda tangan webhook diverifikasi pada setiap permintaan.
372
+ - [ ] Idempotency webhook diimplementasikan.
373
+ - [ ] 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.
374
+ - [ ] Semua panggilan API billing menggunakan kode sisi server saja.
375
+ - [ ] Batas plan diterapkan pada setiap rute yang dilindungi.
376
+ - [ ] Alur dunning pembayaran gagal dikonfigurasi.
377
+ - [ ] Tautan portal pelanggan tersedia dari dalam aplikasi.