Case study · Marketplace
A marketplace for Jamaican vendors and buyers, on the web and on Android. Money goes in, sits in escrow, and only moves when the delivery lands.
Selling online in Jamaica has a trust gap on both ends. The buyer doesn't want to send money to a stranger's account and hope. The vendor doesn't want to hand over goods and hope. Most local commerce solves this by not solving it — a DM, a bank transfer, and a lot of screenshots.
The product had to hold the money in the middle, prove it was holding it, and release it at the right moment. That single requirement decided most of the architecture.
Every account has a wallet, and every wallet movement is double-entry — each transaction writes matching rows, so at any moment the books either balance or the write failed. There is no "adjust the balance" path anywhere in the codebase; balances are derived, never edited.
Payment status is append-only. A database trigger writes every status change into a history table, so an order's payment life — pending, confirmed, released, refunded — is a chain you can read back rather than a single field somebody overwrote. When a customer says "I paid," the answer is in the table, with a timestamp.
Local rails, not Stripe. Bank transfer and Lynk with a receipt upload, reconciled in the admin console — because that is what people here actually use.
The rule I worked to: if you turned off the entire web app and hit the database directly with a signed-in user's token, you should still only be able to see and touch your own rows.
That meant row-level security across all 33 tables — orders, wallets, ledger entries, chat messages, delivery assignments, receipts. Policies are written per role: a buyer sees their orders, a vendor sees orders containing their products, a dispatcher sees only what's assigned to them, admin sees the console. The front end is a convenience layer, not a security layer.
Browsing, cart, checkout, order tracking, vendor dashboard and the admin console — the surface where most of the work gets done.
The client most buyers actually use. Same Postgres, same policies, native where it matters — notifications, camera for receipts, offline-tolerant lists.
Both talk to the same database with the same rules, so a policy fix ships to both at once. There is no second set of business logic to keep in sync, which is the failure mode I was most worried about.
Buyer to vendor, realtime, attached to the order — so the conversation and the dispute evidence live in the same place.
Orders move to a courier with status transitions that the buyer can see, and that the escrow release depends on.
Confirm payments, resolve disputes, release funds, watch the ledger. Boring on purpose.
I'd write the ledger's reconciliation report on day one instead of month three. It existed in my head long before it existed as a page, and every time I wanted to verify a balance early on I was writing the same query by hand. The moment I built the report, three small bugs surfaced in an afternoon that I might otherwise have found from a customer.
I'd also have set up the admin console before the storefront. The storefront is the fun part, but every hour of real operations runs through the console, and building it second meant retrofitting things the ops flow needed.
Payments, marketplaces, admin tooling, mobile. Tell me what you're trying to do.