roedyrustam/vibes-plug/saas-billing

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

SaaS billing state machines, subscription lifecycle, and webhooks / State machine billing SaaS, siklus langganan, dan webhook sinkron.

Install

npx vskill@latest install roedyrustam/vibes-plug/saas-billing

SKILL.md

SaaS Billing Expert (2026 Edition)

English | Bahasa Indonesia


<a name="english"></a>

English

Orchestration & Integration
  • Payment Gateways & Providers: payment-gateway-expert, doku-payment-gateway, agentic-m2m-settlement, agentic-micro-economy-architect
  • Database & Subscription State: database-orm-expert, saas-multi-tenant, saas-architect
  • Security & Post-Quantum Crypto: post-quantum-crypto-migrator, zero-trust-secret-vault, autonomous-red-teamer
  • Background Queues & Notifications: async-queue-temporal-expert, email-notification-expert, sse-websocket-streaming-expert, pdf-document-generation-expert (automated tax invoice & receipt PDF generation)
  • Lifecycle & Governance: production-ready-hardener, brainstorming, zero-to-prod-orchestrator, session-memory-manager
Description

Expert guide for implementing and auditing SaaS billing systems. Covers subscription state machines, secure webhook handling, atomic database synchronization, and the 2026 billing landscape including Stripe, Polar.sh (open-source, developer-first), LemonSqueezy, Paddle, DOKU (Checkout & SNAP BI), and Midtrans (for Southeast Asia).

Trigger Conditions
  • Integrating any payment gateway (Stripe, Polar.sh, LemonSqueezy, DOKU Checkout / SNAP BI, 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 atomic 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
ProviderBest ForOpen SourceMerchant of Record
StripeEnterprise, global, complex billing❌❌
Polar.shDeveloper-first, open-source products✅✅ (optional)
LemonSqueezyIndie hackers, simple pricing, global❌✅
PaddleB2B SaaS, EU VAT compliance❌✅
DOKU (Checkout & SNAP BI)Indonesia & SE Asia enterprise, QRIS, VA, Multi-channel❌❌
MidtransSoutheast Asia / Indonesia❌❌
PayPalGlobal, 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 (Race Condition Prevention)
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(); // Raw body is mandatory
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: Atomic insert on unique constraint to guarantee zero race conditions
try {
await db.webhookEvent.create({
data: {
stripeEventId: event.id,
eventType: event.type,
processedAt: new Date(),
},
});
} catch (err: any) {
// Unique violation (e.g. Prisma code P2002) means this event is ALREADY processed or currently processing
return new Response(JSON.stringify({ message: 'Event already received' }), { status: 200 });
}
// Process event safely knowing this invocation is guaranteed unique
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;
}
}
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");
if (!signature || signature !== process.env.BANK_MUTATION_SECRET) {
return Response.json({ error: "Unauthorized: Invalid signature" }, { status: 401 });
}
const mutations = await req.json();
for (const mutation of mutations) {
if (mutation.type === 'CR' && mutation.amount > 0) {
// Atomic Update: Only update if still pending
const updateResult = await db.transaction.updateMany({
where: {
totalAmount: mutation.amount,
status: 'pending',
provider: 'qris_static',
expiresAt: { gt: new Date() }
},
data: { status: 'paid' }
});
if (updateResult.count > 0) {
const tx = await db.transaction.findFirst({ where: { totalAmount: mutation.amount, status: 'paid' } });
if (tx) {
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, DOKU, 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' | 'doku' | '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(),
});

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
51
Repository forks
10
7-day trend score
20

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/roedyrustam/vibes-plug/saas-billing/badge)](https://verified-skill.com/skills/roedyrustam/vibes-plug/saas-billing)

Something wrong with this skill or its page? Tell us.