A two-sided marketplace matching buyers and sellers with agents, with voice calling in the app
Skelto · mobile marketplace
Real product screens supplied by the team.



The product is a marketplace with two very different sides. On one, someone with a goal — buying a first home, selling, putting money into property. On the other, agents paying a subscription for access to that demand. A marketplace like this is only worth anything if the two sides actually talk, which is why the calling feature was not a nice-to-have. If the conversation moves to exchanged phone numbers, the platform has handed away the only thing it owns.
I worked on the React Native frontend and the Node.js socket layer underneath the real-time parts of the app.
Real-time voice was the problem that took the time.
It is easy to underestimate. You already have a socket connection between two users, so streaming audio over it looks like a small addition. It is not. Voice needs low-latency transport that tolerates packet loss rather than retrying, echo cancellation, jitter buffering, codec negotiation, NAT traversal for two phones on mobile networks, and a call state machine that survives one side losing signal in a lift. Each of those is a specialism, and getting them slightly wrong produces a call that connects and is unusable — which is worse than one that fails, because the user blames themselves and then blames the app.
On top of that, mobile makes it harder than the browser does. The app has to hold audio while backgrounded, interact correctly with the phone's own call handling, and recover when the network moves from Wi-Fi to cellular mid-call.
I built the React Native frontend and the Node.js socket layer behind the live parts of the marketplace — presence, messaging, and the state that both sides see change without refreshing. I also owned the calling work: first the attempt to build it on the existing real-time transport, then the decision to move it onto Agora and the integration that followed.
The decision worth explaining is the one to stop. Continuing to build calling in-house was possible, and it was the wrong call. Voice quality is not a feature you ship at eighty percent — a marketplace whose calls sound bad does not get a second chance with either side, and the agents are paying a subscription for exactly this. Weighing months of specialist work against an SDK maintained by people who do only this, the answer stopped being close.
What that bought was not only working calls. It moved a whole category of future failure — codec issues, one carrier behaving differently, an OS release changing audio session behaviour — onto someone whose job it is to fix it.
Two real-time systems, deliberately separated by responsibility.
The Socket.IO layer on Node carries everything that is a message: presence, chat, and the marketplace state both sides need to see update live. It is a good fit there — ordered, reliable, easy to reason about, and cheap to run.
Calling runs on Agora, which is a media path rather than a message path. The app requests a channel and a token, both sides join, and the media never touches the application server. The socket layer still coordinates the call — the invitation, the accept and decline, the ringing state, the tidy-up when someone hangs up — because that part genuinely is messaging. The audio itself is not.
Drawing the line there was the point. Two systems each doing what they are good at, rather than one doing both adequately.
Signalling stays on the socket layer. Media never touches the application server.
↳ Messages, ordered and reliable
↳ Peer media, not routed through the app server
Every state has an exit, including the ones caused by a phone losing signal.
Invite
Caller emits over the socket; callee receives while backgrounded via push
Ringing
Both sides show state driven by socket events, not by assumption
Accept
Token requested, both clients join the media channel
Connected
Media on Agora; the socket keeps the session state
Interrupted
Network switch or backgrounding — recover rather than drop
Ended
Either side, or a timeout; both systems clean up
The client work was mostly about states that are easy to forget. A call is not connected or disconnected; it is inviting, ringing, accepted, connecting, connected, reconnecting, ended-by-me, ended-by-them, failed. Each needs its own screen state and its own exit, because the ones without an exit are exactly where users get stuck holding a frozen call screen.
The socket layer needed the same discipline. Sockets drop constantly on mobile — a tunnel, a lift, a screen lock — so the client reconnects and reconciles rather than assuming the last state it saw is still true. Presence in particular lies if you trust a disconnect event: a user who reconnects two seconds later was never really offline, and showing them as offline makes the marketplace look empty.
Reanimated carries the interface transitions on the UI thread, which matters most on the call screen — an animation that stutters while a call is connecting reads as the call failing.
The call screen does one job and shows one state at a time, because a call is the moment a user has the least patience for ambiguity. Connection quality is stated in words rather than left to a spinner, and every state — including the failures — has a visible way out.
Stated qualitatively on purpose. Invented percentages are the easiest thing in the world to write and the fastest way to lose a technical reader.
Both sides could talk without exchanging phone numbers, which is the thing a marketplace has to own.
Codec behaviour, carrier differences and OS audio changes became someone else's maintained problem rather than a recurring surprise.
Once it stopped carrying media, it did presence, chat and live state — and did them well.





A marketplace where the two sides can actually speak, built on two real-time systems with a clear line between them — and a calling feature that was handed to specialists at the point where continuing to build it in-house stopped being the responsible choice.
If this sounds familiar
Building something similar?
Most of these problems show up again in different clothes. Describe yours and I will tell you what I would look at first.