Building a Store Without a Payment Gateway — Manual Checkout on Next. js and Supabase
This guide explains how to create a manual checkout system in a Next.js app backed by Supabase. By using a state machine, row‑level security, and server‑side logic, merchants can accept bank transfers or mobile wallet payments without Stripe, while keeping data integrity and audit trails.
In many regions, credit‑card processors like Stripe are either unavailable or prohibitively expensive. Merchants there rely on bank transfers and mobile wallets, and the confirmation of payment is a human‑driven process. Building a checkout flow that accommodates these realities means rethinking the typical payment‑gateway architecture and treating the database as the single source of truth.
Designing the Order State Machine
At the heart of a manual checkout system is a simple, linear state machine that tracks each order’s progress:
- pending – the cart has been submitted, but nothing else has happened.
- awaiting_payment – the customer has received the bank‑transfer details and is preparing to pay.
- payment_submitted – the customer claims they have paid and supplies a reference number.
- confirmed – a staff member matches the reference to a bank statement and marks the order as paid.
- fulfilled – the product has been shipped or delivered.
- rejected – no matching payment was found.
The only state that unlocks further action is confirmed. Treating payment_submitted as fact can lead to fraud; it is merely a customer claim that must be verified.
Database Schema and Security
Supabase exposes tables directly to the browser via a public anon key, so Row‑Level Security (RLS) is mandatory. The orders table might look like this:
id– UUID primary key.reference– unique text used by the customer to identify the transfer.status– text, defaulting topending.customer_name,customer_phone,total_amount,payment_method,payment_reference,confirmed_at,confirmed_by(foreign key toauth.users).created_at– timestamp.
Row‑level policies enforce that anonymous users can only insert orders in the pending state, while authenticated staff can read all orders. All state transitions after insertion are performed server‑side under a service role, ensuring that only the backend can move an order to confirmed or rejected.
Calculating Totals on the Server
Because the client can manipulate prices, the total amount must be computed on the server. The checkout form sends only product IDs and quantities. The server retrieves the current price for each product, multiplies by quantity, and sums the result:
- Fetch product rows by ID.
- Validate each product exists.
- Calculate
total = sum(price * quantity).
Storing the unit price in the order_items table preserves the agreed price even if the product’s price changes later.
Preventing Duplicate Orders with Idempotency
Slow network connections can cause customers to click the submit button multiple times, creating duplicate orders. Generate an idempotency key when the checkout page mounts, include it with the order payload, and enforce a unique constraint on that key. Subsequent submissions return the existing order instead of creating a new one.
Admin Workflow Without Webhooks
Without a payment gateway, the admin panel becomes the critical interface for confirming payments. Key features include:
- Orders sorted by age, so the longest‑waiting customer is addressed first.
- A copy‑to‑clipboard button for the payment reference, making it easy to cross‑check against bank statements.
- Separate “Confirm” and “Reject” actions, each requiring a note and recording the staff member’s ID in an audit table.
- Real‑time status updates sent to the customer via email or SMS, reducing frustration caused by silence.
Because the backend handles all state changes, the system remains reliable even when the front‑end is compromised.
Benefits and Trade‑Offs
Eliminating a payment gateway removes processing fees, chargebacks, and approval hurdles—advantages for small catalogs in markets where card payments are uncommon. The main cost is latency: orders sit in a pending state until a human reviews them. Communicating this delay clearly on the confirmation screen, and providing timely status notifications, mitigates most complaints.
Overall, a manual checkout flow built on Next.js and Supabase can be secure, auditable, and cost‑effective for merchants operating outside the traditional card‑payment ecosystem.
Why it matters
For merchants in regions without reliable card‑payment infrastructure, a manual checkout system offers a viable, low‑cost alternative that still protects against fraud and maintains a clear audit trail.
Key points
- Use a linear order state machine to track progress and prevent premature actions.
- Enable Row‑Level Security in Supabase to protect order data from anonymous users.
- Calculate totals server‑side to avoid client‑side tampering.
- Implement idempotency keys to avoid duplicate orders on slow connections.
- Design an admin panel that clearly shows order status and allows manual confirmation or rejection.
- Communicate expected processing delays to customers to reduce frustration.
Frequently asked questions
Can I still use a payment gateway with this setup?
Yes, you can integrate a gateway for certain payment methods, but the core architecture remains the same—orders still flow through the state machine and RLS policies.
How do I handle refunds?
Refunds can be processed manually by marking the order as refunded in the database and updating the status accordingly, ensuring the audit trail records the action.
What if a customer disputes a payment later?
The audit table records who confirmed the payment and when, providing a clear chain of responsibility for resolving disputes.




