cat ~/production/freelance-marketplace-with-escrow/REPORT.md

Freelance Marketplace with Escrow

Two-sided marketplace for architecture and design work: the platform holds the money until delivery is confirmed, with bidding happening in real-time chat.

role Tech Lead, Backend and Infrastructuretimeline 2025category platformclient An architecture and interior design marketplace

Problem

Design work in this market runs on trust and cash. A client hands money over and hopes; a freelancer delivers and hopes. The platform had to hold the money until delivery was confirmed, run the negotiation in real time, and vet freelancers before they could bid, while also carrying a materials catalogue and a course library in the same product.

Approach

Four systems that would normally be separate products, against one 37-collection schema: a marketplace with escrow, a real-time chat where the bidding actually happens, a supplier materials catalogue, and a course platform whose completion records feed freelancer ranking. The spine is an eight-state deal machine where the escrow balance is not a flag but money physically moved out of the client wallet and parked on the deal document.

Outcome

Live in production. Money moves between three parties, held and released against confirmed delivery, with the platform commission split out atomically on completion. Response time cut from roughly six seconds to two.

37Domain models · largest schema I have shipped
15.4kApplication code · lines, excluding docs
8Deal states · escrow state machine
6s to 2sResponse time · after database profiling
architecture --explain

How it's built

Escrow is the core invariant. Money leaves the client wallet on acceptance and sits on the deal until completion or cancellation, when a narrow transaction splits it between freelancer and platform. Real-time chat drives negotiation, so socket load scales with active deals rather than registered users.

Deliberately narrow transaction boundary on money release

Only the three writes that must be atomic sit inside the session: freelancer credit, platform commission, deal status. Name lookups and notifications happen outside it.

More code than wrapping the whole handler, but the transaction window stays small enough that lock contention never becomes the bottleneck under concurrent completions.

Role-conditional schema defaults for vetting

A freelancer defaults to pending approval, a client defaults to approved. The vetting requirement lives in the schema rather than being remembered in a controller.

Logic in the model layer is unusual, but it makes the invariant impossible to forget at any of the signup paths.

Phone uniqueness enforced in the controller, not the index

A phone number can have many soft-deleted rows and at most one active account, which a unique index cannot express.

The invariant depends on the signup paths checking correctly, so those checks are centralised rather than duplicated.

related --by-stack

Adjacent systems

REL-001 / platform

Vehicle Rental Platform

Rent a scooter or e-bike from an app: scan the QR, the physical lock opens over cellular, ride, park in a permitted zone, pay by the minute from a prepaid wallet.

REL-002 / saas

Multi-tenant Restaurant Platform

A complete restaurant operating system on one multi-tenant backend: customer ordering, driver dispatch, owner dashboard and point of sale.

REL-003 / platform

Game Key Marketplace

A three-sided digital marketplace: a supplier catalogue of tens of thousands of listings, kept in sync, priced in local currency, sold through a payment gateway with automated key delivery.