DEV Community

Cover image for I Got Tired of Renting My Payment Infrastructure, So I Built OwnPay
Fattain Naime
Fattain Naime

Posted on

I Got Tired of Renting My Payment Infrastructure, So I Built OwnPay

If you have ever run a small business online, you know the drill. You sign up for a payment processor, hand over your customer data, watch two to three percent of every sale disappear into fees, and hope the platform does not change its terms next year.

I got tired of that setup. So over the past several months I built OwnPay, a self-hosted, open-source payment gateway platform written in PHP. This post is not a sales pitch. It is a technical walkthrough of why I built it, how it is architected, and what I learned building a system that touches money.

The problem with renting your payment stack

Most payment platforms today are SaaS. You do not own the server, you do not own the database, and you definitely do not own the customer data that flows through checkout. That is fine for a lot of businesses, but it creates three recurring pain points:

  • Fees compound over time. A percentage cut sounds small until your transaction volume grows.
  • Vendor lock-in is real. Migrating away from a processor after two years of integration work is painful.
  • Local payment rails are often an afterthought. If you are building for markets in South or Southeast Asia, mobile financial services like bKash, Nagad, or GCash are not always first-class citizens in Western-built platforms.

I work out of Dhaka, Bangladesh, so that last point hit close to home. Local MFS integrations are usually bolted on as an afterthought or missing entirely. I wanted something that treated them as equally important as Stripe or PayPal.

What OwnPay actually is

OwnPay is a self-hosted payment orchestrator. It is not a payment processor itself. It sits on your own server, between your storefront and whichever gateway your customer chooses, and it handles the parts that are painful to build correctly: checkout rendering, gateway routing, signature and callback verification, ledger bookkeeping, and notifications back to your store.

The flow looks like this:

OwnPay payment flow

A few things that make it different from a typical payment library:

Multi-brand, single install. One OwnPay installation can run multiple isolated brands (stores), each with its own custom domain, gateways, and checkout theme. Isolation happens at the database query layer through a TenantScope trait, so a single super-admin can manage several storefronts without any data bleeding between them. A DomainMiddleware resolves the incoming custom domain and routes the checkout to the right brand, while admin routes stay locked to the master domain.

White-label by default. The checkout page shows your logo, your colors, your domain. OwnPay never appears in the customer-facing experience. That was a deliberate design decision, not an add-on feature.

Gateways as plugins. Every payment gateway is a plugin living under modules/gateways/<slug>/ with a manifest.json and an adapter implementing GatewayAdapterInterface. Out of the box there are 123 built-in gateway integrations, covering global processors like Stripe, PayPal, and Braintree alongside regional ones like bKash, Nagad, SSLCommerz, GCash, Maya, and PromptPay.

Double-entry ledger, not a transactions table. Every payment produces balanced ledger entries, not just a row in a payments table. There is an op_ledger_accounts, op_ledger_transactions, and op_ledger_entries schema that enforces balance constraints at the database level. Refunds and multi-currency conversions post atomically, guarded by mutexes, so you do not end up with a ledger that does not add up.

// Balanced ledger entries for every payment
$ledger->record([
    'debit'  => ['customer_receivable', $amount],
    'credit' => ['merchant_revenue', $amount],
]);
Enter fullscreen mode Exit fullscreen mode

Offline and manual payments are first-class. A lot of self-hosted gateway projects treat bank transfers and manual MFS payments as an edge case. OwnPay has a full manual gateway system for showing bank details or QR codes, plus something I am fairly proud of: an SMS-to-payment confirmation pipeline. A companion Android app parses incoming bank or MFS SMS notifications, matches transaction IDs or amount and timeframe against a pending invoice, and confirms the checkout automatically. It authenticates over JWT with automatic key rotation and instant device revocation, so you are not stuck manually reconciling bank statements against orders.

Why I built a custom core instead of using a framework

This is the question I get most from other developers, so let me address it directly. Why not just build this on top of Laravel or Symfony?

The honest answer is that OwnPay needed a few things that off-the-shelf frameworks do not solve cleanly out of the box: strict multi-brand domain isolation at the query layer, a sandboxed plugin execution model for third-party gateway code, and a domain-specific hook engine similar to WordPress actions and filters. Bending a full framework to do all three well would have meant fighting its conventions more than leveraging them.

Going custom cost more time upfront, but it means I control the entire boot pipeline, there is no dead code from unused framework features, and the security surface is something I own end to end rather than inheriting from a dependency tree I do not fully control. When the product is literally moving money, that tradeoff felt worth it.

Installation is deliberately boring

I did not want OwnPay to require Docker, Kubernetes, or a DevOps background to run. A huge percentage of small businesses and freelancers run on cheap shared hosting or a small VPS, and that had to be a supported first-class path, not an afterthought.

Requirements are simple:

  • PHP 8.3 or later, with bcmath, json, mbstring, openssl, pdo_mysql, and curl
  • MySQL 8.0+ or MariaDB 10.6+
  • Apache with mod_rewrite, or Nginx, pointed at the public/ directory

The release archive is self-contained. No Composer or SSH access is required if you are on shared hosting. You upload the zip, point your domain at public/, and the installer handles the rest.

If you prefer working from source:

git clone https://github.com/own-pay/OwnPay.git
cd OwnPay
composer install --no-dev --optimize-autoloader
cp .env.example .env
# edit .env with your DB credentials and SMTP settings
mysql -u root ownpay < database/schema.sql
php database/seeds/admin.php
Enter fullscreen mode Exit fullscreen mode

Point your web root to public/, and you are running your own payment gateway.

Security is not an afterthought

Anything that touches money needs to be held to a higher bar, and I have tried to treat security as an ongoing process rather than a one-time checklist. Recent work in this area included closing a gap in the webhook pipeline where a gateway's identity was not fully verified, which had opened up a potential server-side request forgery vector. Invoice PDF downloads now sanitize the output filename to prevent path traversal. Permission checks on the brand appearance and theme page were tightened so they correctly require plugin view or manage permissions instead of being reachable by anyone with general admin access.

None of that is glamorous work, but it is the work that matters most in a payment system. If you are evaluating self-hosted financial software, I would encourage you to actually read the changelog and commit history of any project you are considering, not just the marketing page. That transparency is exactly why OwnPay is open source under AGPL-3.0 in the first place. The code, the commits, and the security fixes are all public.

Extending it: gateways, hooks, and the plugin system

If you want to add a payment gateway that is not in the default 123, the plugin architecture is intentionally similar to WordPress, since that is a mental model a huge number of developers already carry around.

A gateway plugin needs:

  1. A directory under modules/gateways/<your-gateway>/
  2. A manifest.json describing the plugin
  3. An adapter class implementing GatewayAdapterInterface

Addons and checkout themes follow the same hook-based extension pattern, using action and filter hooks rather than requiring changes to core files. That separation matters for anyone running a fork in production, since it means upstream updates do not stomp on your customizations.

There is also a REST API for programmatic access and HMAC-signed webhooks for real-time event notifications:

GET /api/v1/payments/op_7Xk9mN2q
Enter fullscreen mode Exit fullscreen mode
{
  "event": "payment.completed",
  "amount": 4999,
  "currency": "USD"
}
Enter fullscreen mode Exit fullscreen mode

Where the project is headed

OwnPay follows what I think of as a "ship when it is ready" approach rather than a fixed release calendar. Docker support is planned for an upcoming release to make local testing and containerized deployment easier. Beyond that, the roadmap is shaped heavily by what self-hosters and contributors actually ask for, since this is a community project first.

If you are building for markets where local payment rails matter as much as global ones, or if you simply do not want a percentage of every sale routed through a third party, I would genuinely like your feedback. Star the repo, open an issue, or better yet, try installing it on a spare VPS this weekend and tell me where it breaks.

Payment infrastructure should be something you own, not something you rent. That is the whole thesis behind this project, and I would love to hear what you think.

Top comments (0)