惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
IT之家
IT之家
H
Heimdal Security Blog
Jina AI
Jina AI
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
T
Tailwind CSS Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
有赞技术团队
有赞技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
AWS News Blog
AWS News Blog
C
Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Hacker News
The Hacker News
The Cloudflare Blog
Hugging Face - Blog
Hugging Face - Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Threatpost
S
Securelist
P
Privacy International News Feed
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 聂微东
博客园 - 叶小钗
J
Java Code Geeks
V
V2EX
博客园 - Franky
Spread Privacy
Spread Privacy
K
Kaspersky official blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Simon Willison's Weblog
Simon Willison's Weblog
Project Zero
Project Zero
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
C
Cybersecurity and Infrastructure Security Agency CISA
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
NISL@THU
NISL@THU
罗磊的独立博客
W
WeLiveSecurity
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园_首页
V
Visual Studio Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Laravel Billing: one package, every gateway, working on day one
Nasrul Hazim Bin Mohamad · 2026-06-01 · via DEV Community

Every SaaS billing integration starts the same way: you pick a provider, pull in their package, wire it up — and three months later when the business wants to add a second gateway (or swap to a Malaysian one like BayarCash or ToyyibPay because Stripe doesn't do local rails), you discover your entire subscription layer is welded to the first provider's package. Different webhook shapes, different status vocabularies, different model assumptions. You're not adding a gateway; you're re-architecting.

I kept watching this happen — especially in the Malaysian market, where the "obvious" global packages assume a gateway that half my clients can't actually use. So I built cleaniquecoders/laravel-billing around one inversion: the gateway is the plugin, not the package. The engine owns subscription and invoice state. A gateway is a single contract your app implements. This post is less about the API surface and more about why it's shaped this way — because the shape is the whole point.

The core decision: one package, gateways as a contract

The temptation when building a billing library is to ship laravel-billing-stripe, laravel-billing-bayarcash, laravel-billing-toyyibpay, and so on. It feels modular. It's actually a maintenance trap — every gateway sub-package re-implements the same subscription lifecycle slightly differently, and the core can never assume a stable shape because each adapter bends it.

This package goes the other way. There is one package, one repo, and it never references a real provider by name. Instead there's a single extension point:

namespace CleaniqueCoders\LaravelBilling\Contracts;

interface PaymentGateway
{
    public function createCheckout(
        Billable $billable,
        Plan $plan,
        PlanInterval $interval,
        string $returnUrl,
    ): CheckoutIntent;

    public function cancel(Subscription $subscription): void;

    public function parseWebhook(Request $request): ?WebhookEvent;
}

Three methods. That's the entire surface your app implements to onboard BayarCash, ToyyibPay, Chip, senangPay, Stripe, or anything else. The trick that makes it hold together is the two DTOs at the boundary — CheckoutIntent going out, WebhookEvent coming back:

final class CheckoutIntent
{
    public function __construct(
        public string $redirectUrl,   // where to send the customer
        public string $externalId,    // echoed back by the webhook for correlation
    ) {}
}

The gateway's job is to translate the provider's idiosyncratic world into these two neutral shapes. Once it does, the engine — subscription transitions, invoice issuance, events — never needs to know which provider it's talking to. The provider-specific mess is quarantined inside one class instead of leaking through your whole billing layer. That's the package-worthy lesson here, independent of billing: when you integrate N external services that do conceptually-the-same thing, define your own DTO at the boundary and make each adapter responsible for the translation. Don't let provider shapes propagate inward.

Batteries included: a gateway that needs no merchant account

Here's the part I'm most pleased with. A fresh install defaults to BILLING_GATEWAY=local, and the bundled LocalGateway runs the entire subscribe → activate → invoice → receipt flow with no real money and no merchant account. You composer require, run migrations, and the billing flow works immediately — in demo, in development, in UAT, in CI.

But it's not a stub. This is the detail that matters:

// LocalGateway::createCheckout — approval flows through the SAME
// WebhookEvent path a real gateway uses
return new WebhookEvent(
    type: WebhookEventType::SubscriptionActivated,
    externalId: $payload['external_id'],
    amountCents: $payload['amount_cents'] ?? null,
    providerEventId: 'local-'.$payload['external_id'],
    rawPayload: $payload,
);

When you click "Approve" on the local dev checkout page, it produces a WebhookEvent and runs it through Billing::handle() — the exact same code path a real BayarCash webhook would take. It even HMAC-signs its checkout token with your app.key and verifies the signature on the way back, so signature-verification logic is exercised too:

public static function verify(string $token): ?array
{
    [$data, $signature] = explode('.', $token, 2);
    $expected = hash_hmac('sha256', $data, static::key());

    if (! hash_equals($expected, $signature)) {
        return null; // tampered or invalid
    }
    // ...
}

Why go to this trouble for a "dev" gateway? Because a fake that takes a different path than production is worse than no fake — it gives you false confidence. By making the local gateway flow through the real activation pipeline, your tests against local actually validate the pipeline a paying customer will hit. Set BILLING_LOCAL_AUTO=true and the whole thing runs synchronously in a single request, which is perfect for CI and feature tests. The local routes also refuse to register in production, so there's no footgun.

Headless core, optional UI

The engine — models, services, contract, events, the manager — works with no UI at all. If you want billing pages fast, there's an opt-in Livewire + Flux UI (plan picker, billing portal, receipt card) that closes the full loop. The guard is clean:

if (config('billing.routes.enabled') && class_exists(Livewire::class)) {
    // register /billing routes
}

If Livewire isn't installed, or you set BILLING_UI_ENABLED=false, the package stays fully headless and you build your own pages against the same models and facade. No hard dependency on the UI stack bleeds into the core. This is the right default for a library: the opinionated convenience layer is there if you want it, but it's behind a class_exists check and a config flag, never mandatory.

The webhook flow, and a replay guard worth stealing

Your app owns the route; the package does the work:

Route::post('/webhooks/{gateway}', function (Request $request, string $gateway) {
    $event = Billing::gateway($gateway)->parseWebhook($request);
    abort_if($event === null, 401);

    Billing::handle($event); // dedups, transitions state, issues invoices, fires events
    return response()->noContent();
});

parseWebhook() (your gateway's code) verifies the signature and normalises the payload, or returns null to reject it. Then Billing::handle() delegates to a WebhookProcessor that replay-guards, locates the subscription, transitions status, issues an invoice on activate/renew, and fires the matching domain event.

The replay guard is a small thing I like:

protected function isReplay(WebhookEvent $event): bool
{
    if ($event->providerEventId === null) {
        return false;
    }

    $key = 'billing:webhook:'.$event->providerEventId;
    $ttl = (int) config('billing.webhook.replay_ttl', 60 * 60 * 24 * 30);

    // Cache::add returns false when the key already exists → replay.
    return Cache::add($key, true, $ttl) === false;
}

Gateways retry. They send the same event twice, three times, because they didn't get your 200 fast enough. If you don't dedup, you double-issue invoices. The neat part is leaning on Cache::add's atomicity — it only writes if the key is absent and tells you whether it won the race, in one operation. No read-then-write window for a concurrent duplicate to slip through. That's a reusable pattern for any idempotent-event handling, not just billing.

State transitions live in one place

WebhookProcessor is where provider events become subscription state, and it reads like a state machine:

match ($event->type) {
    WebhookEventType::SubscriptionActivated => $this->activate($subscription),
    WebhookEventType::SubscriptionRenewed   => $this->renew($subscription),
    WebhookEventType::PaymentSucceeded      => $this->paymentSucceeded($subscription, $event),
    WebhookEventType::PaymentFailed         => $this->paymentFailed($subscription, $event),
    WebhookEventType::SubscriptionCanceled  => $this->cancel($subscription),
};

The gateway's only responsibility is mapping its provider's vocabulary onto these five WebhookEventType cases. Everything downstream — what "activate" means for period dates, when an invoice gets issued, which event fires — is decided once, in the engine, regardless of provider. A SubscriptionStatus enum carries its own access logic so the rule isn't scattered:

public function grantsAccess(): bool
{
    return match ($this) {
        self::Trialing, self::Active, self::PastDue => true,
        self::Canceled, self::Incomplete => false,
    };
}

Note PastDue still grants access — a failed renewal shouldn't instantly lock someone out mid-period. That's a deliberate dunning-friendly choice, and because it lives on the enum, it's consistent everywhere access is checked.

Polymorphic billing: tenancy is optional

The bill target is polymorphic, so the same engine serves single-tenant (User) and multi-tenant (Team/Workspace/Organization) without caring which:

class User extends Authenticatable implements Billable
{
    use HasSubscriptions;
}

HasSubscriptions satisfies the whole Billable contract and gives you the accessors the engine and UI depend on — subscription(), subscribedTo('pro'), onTrial(), onGracePeriod(), plan(), invoices(), plus metered-usage gating via canConsume('seats', 1) / recordUsage('seats', 1). To scope billing to a team instead of the logged-in user, you point one config closure at it:

'billable_resolver' => fn ($request) => $request->user()->currentTeam,

Every UI query and every invoice download is constrained to the resolved billable, and the download routes 403 on a foreign invoice — so one tenant can never see another's invoices. Tenancy didn't require a tenancy feature; it fell out of making the target polymorphic and routing all access through one resolver.

A few more details worth noting

Snapshot vs live. A subscription stores plan_tier as a snapshot string, but the live Plan is resolved from the repository at read time. So plan definitions can live in config or a database table (same PlanRepository interface either way), and a subscriber's tier reference survives even if you restructure your plan models.

Atomic invoice numbers. Sequential numbering (INV-2026-000001) is allocated in a row-locked transaction, so concurrent issuance never collides on a number:

$sequence = $sequenceModel::query()->where('year', $year)->lockForUpdate()->first();
$current = (int) $sequence->next_number;
$sequence->next_number = $current + 1;
$sequence->save();

Malaysia-friendly, neutrally. MYR default, an SST/SSM-aware tax-invoice template, configurable seller details — but all neutral by default, so it's not only a Malaysian package. The tax math is just round(subtotal * rate), stored as a breakdown on the invoice so the PDF renders correctly.

Events as your extension seam. The engine only updates state and issues invoices. Provisioning access, dunning emails, Slack pings — those are your listeners on SubscriptionActivated, SubscriptionRenewed, SubscriptionCanceled, PaymentSucceeded, PaymentFailed, InvoiceIssued. The package doesn't presume to know your side effects.

When you'd reach for this

It fits when you want subscription + invoicing in Laravel and:

  • you need more than one gateway, or a Malaysian gateway, or the freedom to swap later without re-architecting;
  • you want the full flow working on day one — demo, UAT, CI — before any merchant account exists;
  • you want a headless engine you can drive from your own UI, with an optional bundled UI when you're moving fast;
  • you bill teams or workspaces, not just users;
  • you're in a SST/SSM context and want sane local invoicing without a provider lock-in.

If you're all-in on a single global gateway forever and its first-party Laravel package covers you, use that. The value here shows up the moment "which gateway" becomes a question with more than one answer — which, for anyone building for the Malaysian market, it always is.

It's MIT-licensed and on Packagist:

composer require cleaniquecoders/laravel-billing

Repo and full docs (architecture, gateways, the full billing cycle, writing your own driver): github.com/cleaniquecoders/laravel-billing.

Implementing a gateway is one class and three methods — if you write a BayarCash or ToyyibPay driver, I'd love to see it.