vibes-plug 1.0.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 (141) hide show
  1. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  2. package/BLUEPRINT.md +125 -0
  3. package/CHANGELOG.md +195 -0
  4. package/CONTRIBUTING.md +199 -0
  5. package/LICENSE +21 -0
  6. package/README.md +263 -0
  7. package/SECURITY.md +21 -0
  8. package/banner.png +0 -0
  9. package/package.json +25 -0
  10. package/plugin.json +8 -0
  11. package/scripts/update_skills.js +75 -0
  12. package/skills/ai-llm-integration-expert/SKILL.md +162 -0
  13. package/skills/api-design-expert/SKILL.md +310 -0
  14. package/skills/app-analyzer-optimizer/SKILL.md +189 -0
  15. package/skills/asisten_ramah/SKILL.md +41 -0
  16. package/skills/authentication-identity-expert/SKILL.md +45 -0
  17. package/skills/auto-doc-updater/SKILL.md +204 -0
  18. package/skills/bootstrap-to-modern/SKILL.md +87 -0
  19. package/skills/brainstorming/SKILL.md +353 -0
  20. package/skills/bun-runtime-expert/SKILL.md +211 -0
  21. package/skills/ci-cd-devops-architect/SKILL.md +45 -0
  22. package/skills/cloud-hosting-expert/SKILL.md +244 -0
  23. package/skills/coderabbit/SKILL.md +192 -0
  24. package/skills/data-telemetry-expert/SKILL.md +213 -0
  25. package/skills/database-orm-expert/SKILL.md +294 -0
  26. package/skills/design-system-architect/SKILL.md +243 -0
  27. package/skills/e2e-testing-expert/SKILL.md +315 -0
  28. package/skills/event-driven-architect/SKILL.md +81 -0
  29. package/skills/firebase-security-expert/SKILL.md +195 -0
  30. package/skills/fullstack-expert/SKILL.md +202 -0
  31. package/skills/fullstack-expert/references/api_design_guide.md +466 -0
  32. package/skills/fullstack-expert/references/devops_infrastructure.md +477 -0
  33. package/skills/fullstack-expert/references/multi_language_backend.md +528 -0
  34. package/skills/fullstack-expert/references/system_design_patterns.md +358 -0
  35. package/skills/fullstack-expert/scripts/api_contract_validator.py +253 -0
  36. package/skills/fullstack-expert/scripts/architecture_analyzer.py +326 -0
  37. package/skills/gemini-agent-booster/SKILL.md +135 -0
  38. package/skills/global-a11y-i18n-expert/SKILL.md +81 -0
  39. package/skills/go-programming-expert/SKILL.md +295 -0
  40. package/skills/hig/SKILL.md +188 -0
  41. package/skills/js-backend-expert/SKILL.md +192 -0
  42. package/skills/mcp-server-architect/SKILL.md +194 -0
  43. package/skills/mobile-expo-expert/SKILL.md +186 -0
  44. package/skills/monday-design-aesthetic/SKILL.md +67 -0
  45. package/skills/monorepo-architect/SKILL.md +227 -0
  46. package/skills/mpa-orchestrator/SKILL.md +101 -0
  47. package/skills/multi-agent-orchestration/SKILL.md +234 -0
  48. package/skills/multiple-entry-points/SKILL.md +55 -0
  49. package/skills/mvc-expert/SKILL.md +231 -0
  50. package/skills/payment-gateway-expert/SKILL.md +45 -0
  51. package/skills/performance-web-vitals/SKILL.md +332 -0
  52. package/skills/prd-architect/SKILL.md +191 -0
  53. package/skills/production-ready-hardener/SKILL.md +469 -0
  54. package/skills/production-ready-hardener/references/performance_optimization.md +441 -0
  55. package/skills/production-ready-hardener/references/production_checklist.md +161 -0
  56. package/skills/production-ready-hardener/references/security_hardening_guide.md +379 -0
  57. package/skills/production-ready-hardener/scripts/production_readiness_scanner.py +875 -0
  58. package/skills/python-programming-expert/SKILL.md +271 -0
  59. package/skills/realtime-collaboration-expert/SKILL.md +45 -0
  60. package/skills/rust-programming-expert/SKILL.md +235 -0
  61. package/skills/saas-billing/SKILL.md +377 -0
  62. package/skills/saas-multi-tenant/SKILL.md +237 -0
  63. package/skills/saas-mvp-launcher/SKILL.md +231 -0
  64. package/skills/saas-transformer/SKILL.md +446 -0
  65. package/skills/saas-transformer/references/billing_integration_guide.md +401 -0
  66. package/skills/saas-transformer/references/feature_gating_patterns.md +137 -0
  67. package/skills/saas-transformer/references/saas_transformation_checklist.md +121 -0
  68. package/skills/saas-transformer/scripts/saas_transformation_scanner.py +254 -0
  69. package/skills/scalability-clean-code/SKILL.md +229 -0
  70. package/skills/secure-fuzz-testing/SKILL.md +201 -0
  71. package/skills/senior-frontend/SKILL.md +161 -0
  72. package/skills/senior-frontend/references/frontend_best_practices.md +806 -0
  73. package/skills/senior-frontend/references/nextjs_optimization_guide.md +724 -0
  74. package/skills/senior-frontend/references/react_patterns.md +746 -0
  75. package/skills/senior-frontend/scripts/bundle_analyzer.py +407 -0
  76. package/skills/senior-frontend/scripts/component_generator.py +329 -0
  77. package/skills/senior-frontend/scripts/frontend_scaffolder.py +1005 -0
  78. package/skills/senior-fullstack/SKILL.md +167 -0
  79. package/skills/senior-fullstack/references/architecture_patterns.md +160 -0
  80. package/skills/senior-fullstack/references/development_workflows.md +222 -0
  81. package/skills/senior-fullstack/references/tech_stack_guide.md +190 -0
  82. package/skills/senior-fullstack/scripts/code_quality_analyzer.py +114 -0
  83. package/skills/senior-fullstack/scripts/fullstack_scaffolder.py +114 -0
  84. package/skills/senior-fullstack/scripts/project_scaffolder.py +114 -0
  85. package/skills/seo/SKILL.md +225 -0
  86. package/skills/seo/references/cwv-thresholds.md +108 -0
  87. package/skills/seo/references/eeat-framework.md +214 -0
  88. package/skills/seo/references/quality-gates.md +155 -0
  89. package/skills/seo/references/schema-types.md +118 -0
  90. package/skills/seo-aeo-landing-page-writer/SKILL.md +97 -0
  91. package/skills/seo-geo/SKILL.md +188 -0
  92. package/skills/session-handoff-resume/SKILL.md +158 -0
  93. package/skills/skill_baru/SKILL.md +147 -0
  94. package/skills/spa-orchestrator/SKILL.md +288 -0
  95. package/skills/state-management-expert/SKILL.md +272 -0
  96. package/skills/supabase-migration/SKILL.md +45 -0
  97. package/skills/supabase-security-expert/SKILL.md +243 -0
  98. package/skills/tailwind-expert/SKILL.md +188 -0
  99. package/skills/tanstack-query-expert/SKILL.md +199 -0
  100. package/skills/tauri-expert/SKILL.md +97 -0
  101. package/skills/token-saver/SKILL.md +111 -0
  102. package/skills/typescript-expert/SKILL.md +279 -0
  103. package/skills/ui-components-expert/SKILL.md +63 -0
  104. package/skills/ui-ux-pro-max/SKILL.md +201 -0
  105. package/skills/ui-ux-pro-max/data/charts.csv +26 -0
  106. package/skills/ui-ux-pro-max/data/colors.csv +97 -0
  107. package/skills/ui-ux-pro-max/data/icons.csv +101 -0
  108. package/skills/ui-ux-pro-max/data/landing.csv +31 -0
  109. package/skills/ui-ux-pro-max/data/products.csv +97 -0
  110. package/skills/ui-ux-pro-max/data/prompts.csv +24 -0
  111. package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  112. package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  113. package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  114. package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  115. package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  116. package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  117. package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  118. package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  119. package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  120. package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  121. package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  122. package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  123. package/skills/ui-ux-pro-max/data/styles.csv +59 -0
  124. package/skills/ui-ux-pro-max/data/typography.csv +58 -0
  125. package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  126. package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  127. package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  128. package/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-310.pyc +0 -0
  129. package/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc +0 -0
  130. package/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-310.pyc +0 -0
  131. package/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc +0 -0
  132. package/skills/ui-ux-pro-max/scripts/core.py +257 -0
  133. package/skills/ui-ux-pro-max/scripts/design_system.py +493 -0
  134. package/skills/ui-ux-pro-max/scripts/search.py +81 -0
  135. package/skills/ui_ux_expert/SKILL.md +114 -0
  136. package/skills/vibe-code-gardener/SKILL.md +173 -0
  137. package/skills/web-scraper/SKILL.md +205 -0
  138. package/skills/web-scraper/references/data-transforms.md +397 -0
  139. package/skills/web-scraper/references/extraction-patterns.md +475 -0
  140. package/skills/web-scraper/references/output-templates.md +481 -0
  141. package/skills/zero-to-prod-orchestrator/SKILL.md +180 -0
@@ -0,0 +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.
@@ -0,0 +1,237 @@
1
+ ---
2
+ name: saas-multi-tenant
3
+ description: "Design and implement multi-tenant SaaS architectures with RLS, tenant isolation, and PostgreSQL / Desain dan implementasikan arsitektur SaaS multi-tenant dengan RLS, isolasi tenant, dan PostgreSQL."
4
+ author: "Roedy Rustam"
5
+ ---
6
+
7
+ # SaaS Multi-Tenant 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 designing and implementing multi-tenant SaaS architectures with full tenant isolation, Supabase/PostgreSQL Row Level Security (RLS), schema-per-org patterns, RBAC, and Super Admin access controls.
18
+
19
+ ### Trigger Conditions
20
+ - Building a SaaS application that serves multiple organizations (workspaces/tenants).
21
+ - Implementing Row Level Security (RLS) policies in Supabase or PostgreSQL.
22
+ - Designing a data model that isolates tenant data securely.
23
+ - Implementing role-based access control (RBAC) within a tenant.
24
+ - Building the Super Admin management panel for cross-tenant operations.
25
+ - Choosing between shared schema vs. schema-per-org isolation strategies.
26
+
27
+ ### Tenant Isolation Strategies
28
+
29
+ | Strategy | Isolation Level | Cost | Complexity | Best For |
30
+ |---|---|---|---|---|
31
+ | **Shared Schema + RLS** | Row-level | Low | Medium | Standard SaaS (< 1M tenants) |
32
+ | **Schema per Org** | Table-level | Medium | High | Compliance-heavy (HIPAA, finance) |
33
+ | **DB per Org** | Database-level | High | Very High | Enterprise, regulated industries |
34
+
35
+ ### Strategy 1: Shared Schema + RLS (Recommended for Most SaaS)
36
+
37
+ #### Core Schema Design
38
+ ```sql
39
+ -- Central workspaces (tenants) table
40
+ CREATE TABLE workspaces (
41
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
42
+ name TEXT NOT NULL,
43
+ slug TEXT UNIQUE NOT NULL,
44
+ plan TEXT NOT NULL DEFAULT 'free' CHECK (plan IN ('free', 'pro', 'enterprise')),
45
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
46
+ );
47
+
48
+ -- Users belong to multiple workspaces via memberships
49
+ CREATE TABLE workspace_members (
50
+ workspace_id UUID REFERENCES workspaces(id) ON DELETE CASCADE,
51
+ user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
52
+ role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
53
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
54
+ PRIMARY KEY (workspace_id, user_id)
55
+ );
56
+
57
+ -- All tenant data has workspace_id
58
+ CREATE TABLE projects (
59
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
60
+ workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
61
+ name TEXT NOT NULL,
62
+ created_by UUID REFERENCES auth.users(id),
63
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
64
+ );
65
+ ```
66
+
67
+ #### Row Level Security Policies
68
+ ```sql
69
+ -- Enable RLS on all tenant tables
70
+ ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
71
+
72
+ -- Policy: Users can only see projects in their workspaces
73
+ CREATE POLICY "workspace members can view projects"
74
+ ON projects FOR SELECT
75
+ USING (
76
+ workspace_id IN (
77
+ SELECT workspace_id FROM workspace_members
78
+ WHERE user_id = auth.uid()
79
+ )
80
+ );
81
+
82
+ -- Policy: Only admins and owners can create projects
83
+ CREATE POLICY "admins can create projects"
84
+ ON projects FOR INSERT
85
+ WITH CHECK (
86
+ workspace_id IN (
87
+ SELECT workspace_id FROM workspace_members
88
+ WHERE user_id = auth.uid()
89
+ AND role IN ('owner', 'admin')
90
+ )
91
+ );
92
+
93
+ -- Policy: Super Admin can bypass RLS (service role only)
94
+ -- ⚠️ NEVER expose service role key to frontend
95
+ ```
96
+
97
+ #### Session Management & RLS Optimization
98
+ To avoid performance bottlenecks, embed `workspace_id` and `role` directly into the user's Session JWT (Custom Claims). This allows RLS policies to check the session token directly (`auth.jwt()->>'workspace_id'`) instead of joining the `workspace_members` table on every query.
99
+
100
+ #### RLS Helper Functions
101
+ ```sql
102
+ -- Helper: Check if current user has a minimum role in a workspace
103
+ CREATE OR REPLACE FUNCTION user_has_role(
104
+ p_workspace_id UUID,
105
+ p_min_role TEXT
106
+ ) RETURNS BOOLEAN AS $$
107
+ DECLARE
108
+ role_hierarchy TEXT[] := ARRAY['viewer', 'member', 'admin', 'owner'];
109
+ user_role TEXT;
110
+ BEGIN
111
+ SELECT role INTO user_role
112
+ FROM workspace_members
113
+ WHERE workspace_id = p_workspace_id AND user_id = auth.uid();
114
+
115
+ RETURN (
116
+ array_position(role_hierarchy, user_role) >=
117
+ array_position(role_hierarchy, p_min_role)
118
+ );
119
+ END;
120
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
121
+ ```
122
+
123
+ ### Strategy 2: Schema per Org (Compliance-Heavy)
124
+ For regulated industries requiring strict data separation:
125
+ ```sql
126
+ -- Dynamically create a schema for each new tenant
127
+ CREATE OR REPLACE FUNCTION create_tenant_schema(p_slug TEXT) RETURNS VOID AS $$
128
+ BEGIN
129
+ EXECUTE format('CREATE SCHEMA IF NOT EXISTS tenant_%s', p_slug);
130
+
131
+ -- Create all tenant tables in the new schema
132
+ EXECUTE format('
133
+ CREATE TABLE tenant_%s.projects (
134
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
135
+ name TEXT NOT NULL,
136
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
137
+ )', p_slug);
138
+ END;
139
+ $$ LANGUAGE plpgsql;
140
+
141
+ -- Bypass pattern: set search_path per connection
142
+ SET search_path TO tenant_acme, public;
143
+ SELECT * FROM projects; -- Reads from tenant_acme.projects only
144
+ ```
145
+
146
+ ### Super Admin Architecture
147
+
148
+ Super Admin is a separate system that operates **across all tenants** with elevated privileges:
149
+
150
+ ```typescript
151
+ // Super Admin routes are ONLY accessible at admin.yourdomain.com
152
+ // Enforced at DNS + middleware level
153
+
154
+ // middleware.ts — verify super admin domain
155
+ if (hostname === 'admin.yourdomain.com') {
156
+ const session = await verifyAdminSession(req);
157
+ if (!session?.user.isSuperAdmin) {
158
+ return NextResponse.redirect('https://yourdomain.com');
159
+ }
160
+ }
161
+ ```
162
+
163
+ ```sql
164
+ -- Super Admin uses service role to bypass RLS
165
+ -- admin.sql — queries bypass all RLS policies when using service role key
166
+
167
+ -- Cross-tenant query (only accessible with service role)
168
+ SELECT w.name, COUNT(wm.user_id) as member_count, w.plan
169
+ FROM workspaces w
170
+ LEFT JOIN workspace_members wm ON w.id = wm.workspace_id
171
+ GROUP BY w.id
172
+ ORDER BY member_count DESC;
173
+ ```
174
+
175
+ ### RBAC Implementation (TypeScript)
176
+ ```typescript
177
+ type Role = 'owner' | 'admin' | 'member' | 'viewer';
178
+
179
+ const ROLE_PERMISSIONS: Record<Role, string[]> = {
180
+ owner: ['*'], // All permissions
181
+ admin: ['project:create', 'project:delete', 'member:invite', 'member:remove'],
182
+ member: ['project:create', 'project:read', 'project:update'],
183
+ viewer: ['project:read'],
184
+ };
185
+
186
+ function can(userRole: Role, permission: string): boolean {
187
+ const perms = ROLE_PERMISSIONS[userRole];
188
+ return perms.includes('*') || perms.includes(permission);
189
+ }
190
+
191
+ // Usage in API handler
192
+ if (!can(currentMember.role, 'project:delete')) {
193
+ throw new ForbiddenError('Insufficient permissions');
194
+ }
195
+ ```
196
+
197
+ ---
198
+
199
+ <a name="bahasa-indonesia"></a>
200
+ ## Bahasa Indonesia
201
+
202
+ ### Deskripsi
203
+ Panduan ahli untuk merancang dan mengimplementasikan arsitektur SaaS multi-tenant dengan isolasi tenant penuh, Row Level Security (RLS) Supabase/PostgreSQL, pola schema-per-org, RBAC, dan kontrol akses Super Admin.
204
+
205
+ ### Kondisi Pemicu
206
+ - Membangun aplikasi SaaS yang melayani beberapa organisasi (workspace/tenant).
207
+ - Mengimplementasikan kebijakan Row Level Security (RLS) di Supabase atau PostgreSQL.
208
+ - Merancang model data yang mengisolasi data tenant dengan aman.
209
+ - Mengimplementasikan role-based access control (RBAC) dalam tenant.
210
+ - Membangun panel manajemen Super Admin untuk operasi lintas tenant.
211
+ - Memilih antara shared schema vs. schema-per-org.
212
+
213
+ ### Strategi Isolasi Tenant
214
+
215
+ | Strategi | Level Isolasi | Biaya | Kompleksitas | Terbaik Untuk |
216
+ |---|---|---|---|---|
217
+ | **Shared Schema + RLS** | Row-level | Rendah | Sedang | SaaS standar (< 1M tenant) |
218
+ | **Schema per Org** | Table-level | Sedang | Tinggi | Kepatuhan ketat (HIPAA, keuangan) |
219
+ | **DB per Org** | Database-level | Tinggi | Sangat Tinggi | Enterprise, industri teratur |
220
+
221
+ ### Strategi 1: Shared Schema + RLS (Direkomendasikan)
222
+ Rancang tabel `workspaces` (tenant), `workspace_members` (keanggotaan + role), dan semua tabel data dengan kolom `workspace_id`. Terapkan RLS agar pengguna hanya dapat melihat data workspace mereka sendiri.
223
+
224
+ #### Optimasi Session Management & RLS
225
+ Untuk menghindari bottleneck performa, sematkan `workspace_id` dan `role` langsung ke dalam Session JWT pengguna (Custom Claims). Ini memungkinkan kebijakan RLS untuk memeriksa token sesi secara langsung (`auth.jwt()->>'workspace_id'`) daripada melakukan join ke tabel `workspace_members` pada setiap query.
226
+
227
+ #### Fungsi Helper RLS
228
+ Buat fungsi `user_has_role()` yang dapat digunakan kembali di seluruh kebijakan RLS untuk memeriksa apakah pengguna saat ini memiliki role minimum yang diperlukan dalam workspace tertentu.
229
+
230
+ ### Strategi 2: Schema per Org
231
+ Untuk industri teratur yang membutuhkan pemisahan data ketat. Buat schema PostgreSQL terpisah untuk setiap tenant secara dinamis. Atur `search_path` per koneksi untuk mengarahkan query ke schema tenant yang benar.
232
+
233
+ ### Arsitektur Super Admin
234
+ Super Admin adalah sistem terpisah yang beroperasi **di semua tenant** dengan hak istimewa yang ditingkatkan. Hanya dapat diakses di `admin.yourdomain.com` — diberlakukan di level DNS dan middleware. Menggunakan service role key Supabase untuk mem-bypass RLS dan melakukan query lintas tenant.
235
+
236
+ ### Implementasi RBAC
237
+ Definisikan peta izin per role (`owner`, `admin`, `member`, `viewer`) dan fungsi `can()` helper untuk memeriksa izin dalam API handler.