Overview
Payment Service is a centralized billing engine that handles subscriptions for all ProjectLabs products. Each app (BukuBengkel, future products) integrates via API keys and webhooks — create subscriptions, generate invoices, process payments, and notify apps of status changes. No Stripe, no credit cards. Just QRIS — scan and pay.
The Problem
Indonesian small businesses don't use credit cards. Stripe isn't an option. The existing Indonesian payment gateways (Midtrans, Xendit) are overkill for a simple subscription model and their pricing eats into small transactions. We needed a lightweight service that generates QRIS codes, tracks invoice status, and tells each app when a tenant has paid or expired — without coupling billing logic into every product.
What Was Built
A standalone billing microservice with:
- — Multi-app support — each app registers with an API key and webhook URL. Invoices and subscriptions are scoped per app
- — Configurable plans — define plan name, price, duration in months. Each app can have multiple plans (e.g., Bulanan, Tahunan)
- — QRIS payments — generates QRIS strings via Pakasir API, rendered as QR codes on each app's pay page
- — Webhook-driven activation — Pakasir sends payment confirmation, the service updates invoice status, activates the subscription, and notifies the target app via webhook
- — Grace period management — when a subscription expires, it enters a grace period (configurable days) before suspension. Daily cron job handles status transitions and auto-generates renewal invoices
- — Admin dashboard — manage apps, plans, subscriptions, and view invoice history from a single panel
Integration Flow
When a tenant registers on BukuBengkel and selects a plan, BukuBengkel calls the Payment Service to create a subscription and invoice. The service returns a QRIS payment URL. BukuBengkel renders a QR code on the pay page. The tenant scans and pays. Pakasir sends a webhook to the Payment Service, which marks the invoice as paid, activates the subscription, and fires a webhook back to BukuBengkel with the new status. BukuBengkel updates the tenant's access. The whole cycle runs without manual intervention.
Technical Notes
- — Stack — Next.js 16 (App Router), React 19, TypeScript, Drizzle ORM, SQLite (better-sqlite3), better-auth
- — Payment gateway — Pakasir API for QRIS transaction creation and webhook callbacks
- — Cron — daily check script that evaluates all subscriptions: expiring soon (generate renewal invoice), expired (set grace period), grace period over (suspend)
- — Webhooks — outgoing to registered apps on status change, incoming from Pakasir on payment confirmation
- — Self-hosted — co-deployed on the same VPS via PM2 + Caddy, port 5005