Skelto matches buyers and sellers with agents, with voice calling in the app
Skelto · mobile marketplace
Skelto mobile app screens.



Skelto 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 product like Skelto 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.
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.





Skelto lets the two sides 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.