October 4, 2026 · Magati Joel
Building a Reusable Payment Engine for Multiple SaaS Products
How I designed a reusable payment infrastructure layer to handle payment initiation, verification, webhooks, idempotency and billing integrations across multiple applications using Supabase, PostgreSQL, Flutter and IntaSend.

Why I Built a Payment Engine
As I continued building multiple applications, I noticed that payment integration was becoming a repeated problem.
Each application needed to handle payments, transaction states, payment verification, subscription access and payment notifications.
Building that logic independently inside every application would work, but it would also create duplicated code, duplicated security concerns and multiple places to maintain payment-provider integrations.
I wanted to solve the problem at the infrastructure level.
Instead of making every application communicate directly with a payment provider, I designed a reusable payment engine that could sit between my applications and the payment infrastructure.
The goal was simple:
Build the payment logic once, make it reusable, and allow multiple applications to consume it without tightly coupling their databases or application logic.
The Architecture
The resulting architecture separates the application layer from the payment infrastructure.
Applications communicate with the payment engine through secure server-side integrations.
The payment engine is responsible for the payment lifecycle, while each application remains responsible for its own users, business data and application-specific subscription experience.
At a high level, the architecture looks like this:
Application ↓ Application Billing Bridge ↓ Payment Engine ↓ Payment Provider ↓ Webhook ↓ Payment Engine Verification ↓ Application Billing State
This separation allows the same payment infrastructure to support multiple products without turning those products into one tightly coupled system.
Designing for Multiple Applications
One of the main requirements was supporting more than one application from the same payment infrastructure.
The applications should share payment capabilities without sharing their application databases.
For example, Farmora, InvoiceEasy and Landlord Ledger can each have their own:
- Authentication
- Users
- Business data
- Application database
- Subscription interface
- Local billing projection
while using the same centralized payment infrastructure underneath.
This creates a useful separation between application ownership and payment processing.
The payment engine knows which product initiated a payment and what resource the payment belongs to, while the application continues to own its own business logic.
Payment Provider Abstraction
I also wanted to avoid making the applications dependent on a single payment provider implementation.
The payment engine therefore acts as a provider-neutral layer.
The application does not need to know the internal implementation details of the payment provider.
Instead, it requests a payment through the payment engine, which handles the provider-specific interaction.
This makes it possible to introduce additional providers or fallback mechanisms without rewriting the billing logic inside every application.
For the Kenyan payment environment, IntaSend is currently used for hosted checkout and M-Pesa payment processing.
The Most Important Part: Verification
One of the most important design decisions was separating payment notification from payment confirmation.
A webhook saying that a payment was completed should not automatically mean that a user's subscription is activated.
Instead, the payment engine validates the transaction before allowing it to affect billing state.
The general flow is:
Payment initiated ↓ Provider processes payment ↓ Webhook received ↓ Server-side verification ↓ Validate payment reference ↓ Validate amount and currency ↓ Check payment state ↓ Apply idempotency protection ↓ Record payment ↓ Allow billing state to change
This creates a much safer boundary between the external payment provider and application access control.
Idempotency and Duplicate Events
Payment systems also have to deal with something ordinary application CRUD operations don't always encounter: the same event can arrive more than once.
Network failures, retries and webhook delivery mechanisms can result in duplicate notifications.
The payment engine therefore treats payment processing as an idempotent operation.
A payment that has already been processed should not result in another subscription activation or another payment being recorded simply because the provider sent the notification again.
This was an important part of making the system reliable rather than simply functional.
Hosted Checkout
Another design decision was to keep sensitive payment-provider operations on the server side.
Applications can request a payment and receive the appropriate checkout information without exposing provider credentials to the Flutter application.
The user then completes the payment through the hosted payment experience.
This keeps secrets and provider-specific credentials outside the client application.
It also gives the payment provider control over sensitive payment collection rather than requiring the application itself to handle payment credentials.
Manual Payment Support
Not every payment needs to happen through an automated checkout.
For environments where manual M-Pesa payments are still useful, the payment engine also supports a manual payment workflow.
A manually submitted payment is treated as pending rather than immediately granting access.
The payment can then go through the appropriate verification and approval process before billing access changes.
This makes the system useful in environments where both automated and manual payment methods are required.
Keeping Applications Independent
A major goal was avoiding a situation where the payment engine becomes responsible for the entire application.
The payment engine handles payment infrastructure.
The application handles the user experience.
For example, InvoiceEasy can continue managing its own invoice data, customers, businesses and application-specific subscription presentation.
Farmora can continue managing farm operations independently.
The payment engine simply provides a shared financial infrastructure layer.
This keeps the applications modular and makes it possible to evolve them independently.
Building Around Supabase
The infrastructure is built around Supabase and PostgreSQL.
Supabase provides the backend foundation while PostgreSQL provides the persistent billing and transaction data layer.
Server-side Edge Functions handle communication between applications and external payment services.
This architecture also allows sensitive operations to remain on the server rather than inside Flutter clients.
The applications themselves are built primarily with Flutter and Dart.
Designing for Failure
A payment system cannot assume that every request will succeed.
Payments can remain pending.
Webhooks can be delayed.
Providers can temporarily become unavailable.
Networks can fail.
A user can close a browser immediately after initiating payment.
Because of this, the payment flow was designed around explicit payment states rather than treating payment as a single request that either succeeds or fails.
This makes it possible to reconcile payment state later instead of assuming that a temporary failure means the payment never happened.
Testing the Payment Flow
Before moving toward live payments, I tested the payment lifecycle using the provider's sandbox environment.
The testing process covered payment initiation, hosted checkout, M-Pesa processing, provider responses, webhook delivery, verification and idempotency.
One useful lesson from testing was that a successful payment and a successful webhook delivery are two different things.
A payment can successfully complete at the provider while the notification path experiences a temporary failure.
Designing around that distinction is important for building reliable payment infrastructure.
Security Principles
The payment engine follows a few important principles:
- Payment provider secrets remain server-side.
- Client applications never receive service credentials.
- Payment callbacks are not treated as unconditional proof of payment.
- Payments are verified before affecting billing state.
- Duplicate payment events are handled idempotently.
- Products are isolated from each other.
- Application databases remain independent.
- Manual payments remain pending until verified.
- Sensitive infrastructure configuration is kept outside the client.
These principles are more important to me than simply making the payment button work.
What I Learned
Building the payment engine changed how I think about payment integration.
The difficult part isn't calling a payment API.
The difficult part is everything around that API.
What happens when a webhook arrives twice?
What happens when the payment succeeds but the application doesn't receive the notification?
What happens when a user closes the checkout page?
What happens when the provider temporarily becomes unavailable?
What happens if a client tries to claim that a payment succeeded?
These questions pushed the implementation from a simple payment integration toward a reusable payment infrastructure system.
The Result
The result is a centralized payment layer that can be reused by multiple applications while keeping those applications independent.
Instead of implementing payment-provider logic separately in every product, new applications can integrate through a controlled billing bridge and reuse the existing payment infrastructure.
The architecture is designed to grow with the product ecosystem rather than forcing every application to reinvent payment processing.
For me, this project has been less about building a payment button and more about learning how to design reliable infrastructure around financial transactions.
Technology Stack
- Flutter
- Dart
- Supabase
- PostgreSQL
- Supabase Edge Functions
- IntaSend
- M-Pesa
- REST APIs
- Server-side payment verification
- Webhooks
- Idempotent transaction processing
Final Thoughts
Building this payment engine has been one of the more technically interesting parts of my recent development work.
It sits behind the applications rather than being something users necessarily see, but it solves an important problem: making payment processing reusable, secure and predictable across multiple products.
The next step is continuing to improve provider resilience, reconciliation, observability and the developer experience for applications integrating with the platform.