All work

Case study · Marketplace

Coronation Market

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.

RoleEverything — design, backend, web, Android
StackReact, Supabase / Postgres, Kotlin
StatusLive — web + Google Play
Timeline2024 – ongoing

The problem

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.

If the ledger can drift, nothing else you build on top of it matters.

How the money works

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.

Order lifecycle

Cart Order created Payment + receipt upload Escrow held Dispatch Delivered Vendor paid out

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.

Security that isn't in the front end

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.

  • Order IDs are random rather than sequential, so nobody can guess the next one or count your volume.
  • Receipt uploads land in scoped storage with policies matching the order's owner.
  • Admin actions are logged as data, not as console output.

One product, two clients

Web

React storefront

Browsing, cart, checkout, order tracking, vendor dashboard and the admin console — the surface where most of the work gets done.

Android

Kotlin app on Play

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.

The rest of the system

Chat

In-app messaging

Buyer to vendor, realtime, attached to the order — so the conversation and the dispute evidence live in the same place.

Dispatch

Delivery assignment

Orders move to a courier with status transitions that the buyer can see, and that the escrow release depends on.

Admin

Operations console

Confirm payments, resolve disputes, release funds, watch the ledger. Boring on purpose.

What I'd do differently

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.

What it demonstrates

  • I can hold money safely. Escrow, double-entry, append-only history, per-row authorization.
  • I can carry a product end to end. Schema, policies, two clients, admin tooling, Play Store release.
  • I build for where it runs. Local payment rails and patchy connections, not a US-market assumption copy-pasted onto Kingston.

Need something built to this standard?

Payments, marketplaces, admin tooling, mobile. Tell me what you're trying to do.