Hazem Salama
Hazem Salama
An on-demand services marketplace for Saudi Arabia, shipped as five products on one Node/Prisma backend with in-app escrow.
Sandlee connects clients who need a home or business service with verified, KYC-approved providers. A client posts a request, nearby providers bid, the client pays into in-app escrow, the job runs behind two verification codes, and money releases on completion. It was designed and built solo as five coordinated products on a single Express 5 + Prisma 7 backend, with 35 data models, 165 REST endpoints, and a shared rules package keeping every app in lockstep.
~1M SAR
Revenue in a 2-month soft launch
5
Products on one backend
165
REST endpoints
35
Prisma data models


Live tracking, escrow checkout, and on-platform chat, from the production client app.
The problem
Saudi Arabia's services market runs on word-of-mouth and WhatsApp. A homeowner needs a plumber, asks a group chat, gets a number, and hopes for the best: no price discovery, no vetting, no recourse if the work is bad. Skilled providers, meanwhile, have no reliable pipeline and no portable reputation.
The hard part is not the listing, it is everything around the transaction. Money has to be safe for the client without stranding the provider. Communication has to stay on-platform or the marketplace gets disintermediated the moment two people swap numbers. And every flow has to be Arabic-first and RTL, on Saudi payment rails, built fast enough to validate the market, by one engineer.
The approach
Thin clients around a single Express / Prisma API. All four apps import the same pinned package, @sanad/shared, the single source of truth for enums, business rules, and Zod schemas, so logic physically cannot drift between client, provider, admin, and server.
01
Request, compare quotes, pay into escrow, live-track, chat, rate.
React Native 0.83 · Expo 55
02
KYC onboarding, live job board, quotes, background GPS, earnings.
React Native 0.83 · Expo 55
03
15 feature pages: KYC, disputes, payouts, KPIs, analytics.
Next.js 16 · React 19
04
Standalone Arabic / RTL marketing site and legal pages.
Next.js 16 · Tailwind
05
165 endpoints, Socket.IO, BullMQ, the payment state machine.
Express 5 · Prisma 7
What I built
Funds are authorized when the client picks a quote, captured only on verified completion, voided or refunded on cancellation. Escrow is an application-layer state machine, not a gateway feature.
One order can be paid wallet-only, card-only, or a wallet + card split. Only the card remainder hits Moyasar; amounts compare in halalas to avoid float drift, and one Payment row stays the source of truth even with mid-job add-ons.
The provider app broadcasts location every 15s while a job is active (background task + Android foreground service), posted over REST and relayed to the client map over Socket.IO.
A normalization-first filter converts Arabic-Indic and Persian digits to ASCII, masks phones, emails, URLs and messaging handles, and OCR-scans chat images, all to keep deals on-platform. Violations escalate on a strike ladder.
A six-step onboarding wizard uploads national ID, commercial registration, and IBAN docs to Cloudinary; admins approve or reject in a document viewer. Status gates the whole provider app.
A 48-hour window, audience-scoped evidence, and an admin "gavel" panel that splits the held amount into a client refund and provider payout via a 0–100% slider, with discipline attached in the same action.
Money as a state machine
The order lifecycle drives the money. Every transition is enforced server-side and wrapped in a database transaction, with deterministic idempotency keys so a webhook retry or a double-tap can never double-post.
Order lifecycle · 15 states
Payment lifecycle · 9 states
Architecture
Socket.IO carries chat, presence, quote arrivals, and provider GPS. BullMQ handles what should run on a clock: quote expiry, auto-release of held funds, tier recalculation, and a tiered order fan-out that gives Gold providers a head start.
Clients
API / Edge
Async · BullMQ
Data & integrations
Engineering deep dives
A 9-state payment lifecycle and a typed ledger with deterministic idempotency keys, so a replayed webhook or double tap is a no-op. Webhooks dedupe on a unique event id; authorize amounts compare in halalas; quote acceptance, order transition, code minting, and the timeline write all run in one transaction.
A normalization-first text filter converts every digit script to ASCII before applying separator-tolerant regexes for Saudi mobiles, long digit runs, emails, URLs, and messaging handles. Images get an upload-time OCR pass that deletes them before any sendable URL exists if they carry contact info.
On creation, match approved, online, specialized providers; push to Gold tiers immediately and release everyone else via a delayed BullMQ job for a fair head start, with an immediate-emit fallback if Redis is down. Distance is Haversine in app code, no PostGIS dependency.
@sanad/shared defines every enum and constant once as Zod schemas (the TS type comes free), pinned by commit SHA in all four consumers, with Renovate auto-PRing the bump everywhere on change. The backend even validates outgoing responses against the same schemas in dev.
Inside the product · 01
The demand side: request a service, compare competing quotes, pay into escrow, and stay on-platform. Every contact detail in chat is masked automatically. These are real screens from the production app.




Inside the product · 02
The supply side: a live job board, quoting with a commission preview, KYC onboarding, and a fully code-gated job flow. A distinct slate and emerald palette, by design. Real screens from the production provider app.




Inside the product · 03
The operator's seat: resolve disputes by splitting held funds, track KPIs by city, and control coverage on a map. Real screens from the production admin console, with 21+ feature pages in all.

The public face
Built RTL-first with no animation library, just CSS and IntersectionObserver. Real screens from the live marketing site.

The results
~1M SAR
Revenue in a 2-month soft launch
165
REST endpoints across 17 groups
35
Prisma models · 56 indexes
5
Products on one shared backend
Engineering metrics verified from the codebase. Revenue is a reported business result from the soft-launch period.
Let's build the next one