Hazem Salama
All work

Sandlee سَنَدليArchitect and lead engineer2026 · ~3 months, solo

An on-demand services marketplace for Saudi Arabia, shipped as five products on one Node/Prisma backend with in-app escrow.

Stack: React Native 0.83, Expo SDK 55, Next.js 16, React 19, Express 5, Prisma 7, PostgreSQL, Redis, BullMQ, Socket.IO, Zod, Moyasar, Apple Pay, Cloudinary, Firebase FCM, Google Maps

Trust, for two audiences at once

The problem

Saudi Arabia's services market runs on word-of-mouth and WhatsApp. A homeowner asks a group chat for a plumber, gets a number, and hopes for the best: no price discovery, no vetting, no recourse if the work is bad. Providers have no reliable pipeline and no portable reputation.

The listing is the easy part. Money has to be safe for the client without stranding the provider, and conversation has to stay on-platform or the marketplace is bypassed the moment two people swap numbers.

All of it had to be Arabic-first and RTL, on Saudi payment rails, and built by one engineer fast enough to validate the market.

Decisions

What I chose, what I chose it over, and what it cost

Sandlee is five products on one backend: a client app and a provider app (React Native, Expo), an admin console and a marketing site (Next.js), and the API (Express, PostgreSQL, Redis, BullMQ).

Around the transaction sit real-time GPS tracking, Socket.io chat, Moyasar hold and capture payments, provider KYC, and 5-role access control. Three choices shaped the rest.

Hold the money in the application, not the gateway

ChoseMoyasar only authorizes and captures the card. The hold itself is an application state machine: funds are authorized when the client picks a quote, captured only on verified completion, and voided or refunded on cancellation. Every transition is enforced server-side inside a database transaction, and ledger entries carry deterministic idempotency keys, so a replayed webhook or a double tap is a no-op.
OverCharging the card when the quote is accepted and refunding when a job falls through. It is simpler to build, but the client pays before any work is verified and every cancellation becomes a refund.
CostThe platform owns the correctness of the money. That meant modelling a payment lifecycle, deduplicating webhooks on a unique event id, comparing amounts in halalas to avoid float drift, and running a scheduled job to auto-release held funds.

One backend and one shared rules package

ChoseEvery product is a thin client around a single Express and Prisma API. Enums, business rules and Zod schemas are defined once in @sanad/shared and pinned by commit SHA in the client app, provider app, admin console and API, so a rule cannot mean two things in two codebases. In development the backend also validates its own responses against the same schemas.
OverEach codebase keeping its own copy of the enums, constants and validation. That is the faster start for one engineer, and it is how an order status ends up meaning one thing in the provider app and another on the server.
CostA rule change is a package release, not a one-line edit. The pinned SHA has to be bumped in every consumer, which Renovate does by opening a PR in each one. A single API is also one dependency shared by every product.

Filter contact details out of chat before delivery

ChoseA marketplace is bypassed the moment two people swap numbers, so chat is filtered on the way in. The filter normalizes first: Arabic-Indic and Persian digits become ASCII before separator-tolerant patterns look for Saudi mobile numbers, long digit runs, emails, URLs and messaging handles. Chat images get an OCR pass at upload and are deleted before a sendable URL exists if they carry contact info. Violations escalate on a strike ladder.
OverOpen chat backed by terms of service and after-the-fact reports, or plain patterns on the raw text. The first acts after the number has already been shared. The second is beaten by typing the number in Arabic digits or sending a screenshot.
CostMatching long digit runs will sometimes mask a legitimate number, so the app warns before sending and violations surface in the admin console. OCR adds a processing step to every image upload.

The order drives the money

Money as a state machine

When the money moves
Order eventPayment becomes
Client picks a quoteAuthorized
Completion verifiedCaptured
Order cancelledVoided or refunded

Order lifecycle

  1. 1New
  2. 2Quotes received
  3. 3Provider selected
  4. 4Payment held
  5. 5En route
  6. 6Work started
  7. 7Completed
  8. 8Confirmed
  9. 9Closed

Payment states

  • Pending
  • Authorized
  • Captured
  • Voided
  • Refunded
  • Disputed
  • Settled

1M SAR ($270K) in transactions during a 2-month soft launch

The result

Built in about three months by one engineer, and in production.

The brief was to validate the market quickly, and the soft launch did that with real money moving through the in-app escrow described above, not through a group chat and a bank transfer.

Screens

Captured from the production apps, one set per surface

Client app

Request, compare quotes, pay into escrow, track, rate.

Client: competing quotes with price, rating, and ETA
Compare quotes
Client: tracking screen showing the start-work verification code
Start-work code
Client: before/after photos and the completion code gate
Completion code and photos
Client: three-axis blind rating for quality, time, and cleanliness
Three-axis blind rating

Provider app

KYC onboarding, job board, quoting, and a code-gated job flow.

Provider: map of nearby requests with distances and an online toggle
Live job board
Provider: quote with price, ETA, and a 20% commission and net-payout preview
Quote with commission preview
Provider: entering the four-digit start-work code from the client
Code-gated start of work
Provider: KYC onboarding, uploading ID or commercial registration
KYC · documents

Admin console

Disputes, payouts, KYC review, and the ledger.

Admin: three-panel dispute resolver with the refund-split slider and chat log
Dispute resolution: split the held amount
Financial ledger with the Settlement Runner and payment states
Financial ledger
Provider KYC drawer: documents, rating, and tier control
Provider KYC & tier
Violations and warnings raised by the content filter
Violations from the chat filter

Marketing site

Standalone, Arabic and RTL-first.

Sandlee marketing site hero: you request, the professional comes to you
Marketing site hero
Your money does not move until you approve: escrow explainer
Escrow explainer

By the numbers

How the figures on this page are defined

Engineering counts
Engineering countnote 2Number
Products on one backend5
REST endpoints165
Data models35
  1. 1The transaction figure is a reported business result from the soft-launch period.
  2. 2Engineering counts are verified from the codebase.