FixAm

A two-sided marketplace connecting Ghanaian homeowners with vetted tradespeople.

A service professional at work.

FixAm connects customers with service professionals across Ghana. Web at fixam.pro, plus an iOS app.

The product argument belongs on its own site. This is the build.

The constraint

A marketplace has to solve one side before the other, and we chose supply. An empty directory is worth nothing to a customer, whereas a professional with a public profile and a shareable link gets value on day one whether or not anyone searches yet. That ordering decided the roadmap: the profile, the work gallery, the QR business card and the dashboard all shipped before anything aimed at buyers.

The harder constraint is trust. A review system on a marketplace is attacked from the moment it exists, usually by the professionals themselves. There is no customer account to hang a review on, because forcing a sign-up to leave a review means no reviews at all.

The stack, and why

Backend is Next.js on Railway with Prisma over Postgres. The app is Expo SDK 54 with React Native 0.81, React 19, React Navigation 7, Zustand for local state and TanStack Query for server state. Images go through Cloudinary. SMS goes through Arkesel.

Cloudinary is configured with incoming transforms rather than delivery transforms: profile photos are cropped to a 640 square on the face, covers to 1300x500, gallery work to a 1200 limit, and the originals are discarded at upload. That is a one-way door and we walked through it deliberately. Storing originals for a marketplace of phone photographs is a cost with no product behind it. Identity document folders are excluded from that pipeline and are never transformed.

a review flow, left to right. `Customer taps review` -> `POST send-otp` -> `Arkesel SMS` -> `code entered` -> a diamond labelled `self-review? same phone in 60 days?` with a rejected branch -> `review stored`.

Three decisions

Reviews are verified by SMS, not by an account. Leaving a review sends a six-digit code to the reviewer's phone through Arkesel, and the review is only accepted with a matching code. Self-review is blocked by matching the reviewer's number against the professional's phone and WhatsApp number. The same phone can review the same professional once per 60 days. The trade-off is that every review costs an SMS and adds a step that loses some honest reviewers. What we get is a rating that means something, which is the only reason a directory beats a WhatsApp group.

Search ranking is fixed and boring. Review count descending, then rating descending, then account age ascending, applied identically on the API and on the public pages. No paid placement, no featured slots. The homepage briefly carried a featured-professionals strip and it came out again: a free ad for whoever we picked is not a ranking, and it teaches the supply side to lobby instead of to work.

Certifications are a separate, gated object. A professional can attach up to four pages of a credential document. Those go to a private folder with no incoming transform, an admin queue holds them for review, and the public API returns only the approved credential name and issuer. The document URL never leaves a guarded endpoint. On the public profile it renders as a gold chip beside the trade, kept visually distinct from the green identity tick, because "we checked their plumbing certificate" and "we checked who they are" are different promises.

That distinction is about to get more expensive. Ghana's L.I. 2523, effective June 2026, requires real-time biometric verification against the national identity record and restricts retaining card photographs. The manual card-photo review FixAm ships today is on the wrong side of that, so the migration path is a biometric check against the NIA record, contracted under BestDid Technology Ltd, our Ghana-registered entity.

What broke

Uploads froze on "Uploading..." with no error. Not slowly, not sometimes: a photograph taken on a modern phone would hang, and the page would sit there forever.

The cause was two faults stacked. Phone camera files are large enough that the request stalls against the proxy and never gets an HTTP response back, and the upload handlers had no timeout and no finally, so the spinner had nothing to turn it off. A 29.7MB test file reproduced it every time.

The fix was a browser-side helper that downscales to 2200px JPEG at quality 0.85 before upload, with a 90-second abort and real error messages, wrapping every upload on the page: profile, cover, identity, work and certificates. The part worth keeping is not the resize. It is that a request with no timeout is a bug even when the network is fine, and it presents as a UI problem so it gets filed against the wrong team.

Where it is now

Live on iOS since 2026 and on the web, with a force-update gate that lets us raise a minimum version from an environment variable instead of shipping a build. Certifications, the analytics dashboard and universal links are shipped.

One App Store note worth passing on, because it cost us a rejection under Guideline 2.3.7: Apple treats "free", "no commission" and "no booking fees" as price references in screenshots, including the positive ones. Price talk belongs in the description, not on the images.

Built by BestDid

We built FixAm and we run it. To use it, or to list a business on it, go to fixam.pro.

Two neighbours: the PadiRent case study covers phone-and-OTP auth on unreliable networks, and the Vendwa case study covers a tool-use agent on a messaging API. Everything else is on our work page. To discuss a build, see services.

Visit FixAm