Developing a Mobile App for Freelance Marketplace
Freelance marketplace is two-sided with escrow payments, chat, bidding system, and reputation mechanisms. Technically more complex than most eCommerce apps: two user types with fundamentally different interfaces, escrow as financial instrument, and dispute arbitration.
Two-Sided Marketplace Architecture
Clients and freelancers—different roles in one account or separate entities, depends on concept. Two approaches:
Separate Accounts—like Upwork: "Hire" (client) and "Find Work" (freelancer). Cleaner permissions management, but user can't be both.
Single Account with Role Switching—like Kwork: one profile, context switch. Technically: userRole: 'client' | 'freelancer' in session, different navigation trees.
// iOS: navigation by role via enum
enum UserContext {
case client
case freelancer
}
class AppCoordinator {
var currentContext: UserContext = .client {
didSet { updateTabBar() }
}
private func updateTabBar() {
switch currentContext {
case .client:
tabBarController.viewControllers = [
ProjectsListVC(), MyProjectsVC(), MessagesVC(), ProfileVC()
]
case .freelancer:
tabBarController.viewControllers = [
FindProjectsVC(), MyOrdersVC(), MessagesVC(), ProfileVC()
]
}
}
}
Escrow: Protected Payments
Marketplace's main feature—platform holds funds until task completion. Scheme:
- Client creates project and deposits sum to escrow account (charge/capture)
- Freelancer completes work, delivers result
- Client accepts → platform releases funds to freelancer minus commission
- If dispute → arbitration (manual or automatic)
- If client unresponsive N days → auto-confirm
# Stripe Connect: split payment with auto-platform commission
import stripe
def release_payment_to_freelancer(escrow_payment: EscrowPayment) -> None:
platform_fee = int(escrow_payment.amount * Decimal("0.10")) # 10% commission
# Create transfer to freelancer
transfer = stripe.Transfer.create(
amount=escrow_payment.amount - platform_fee,
currency="usd",
destination=escrow_payment.freelancer_stripe_account_id,
transfer_group=escrow_payment.project_id,
metadata={
"project_id": escrow_payment.project_id,
"order_id": escrow_payment.order_id
}
)
db.update_escrow(
escrow_payment.id,
status="RELEASED",
transfer_id=transfer.id
)
notify_freelancer(escrow_payment.freelancer_id, "payment_released", transfer.amount)
Partial payment (milestone payments): large project split into stages, each with separate escrow. Freelancer gets money as stages accepted—reduces risk for both.
Bidding System
Freelancers bid on projects: offer price and timeline. Client sees offer list, candidate portfolios, ratings.
// Android: bid list with sorting
data class Bid(
val id: String,
val freelancerId: String,
val amount: Decimal,
val deliveryDays: Int,
val coverLetter: String,
val freelancer: FreelancerProfile
)
@Composable
fun BidsList(
bids: List<Bid>,
sortBy: BidSortOption,
onHire: (Bid) -> Unit
) {
val sorted = when (sortBy) {
BidSortOption.PRICE -> bids.sortedBy { it.amount }
BidSortOption.RATING -> bids.sortedByDescending { it.freelancer.rating }
BidSortOption.DELIVERY -> bids.sortedBy { it.deliveryDays }
}
LazyColumn {
items(sorted, key = { it.id }) { bid ->
BidCard(bid = bid, onHire = { onHire(bid) })
}
}
}
Bid limits: junior freelancers get N free bids/month, after—paid. Standard marketplace monetization model.
Built-in Chat
Chat between client and freelancer—mandatory. Requirements: message delivery, statuses (sent/read), files and images, task links in conversation context.
Technically: WebSocket for real-time + offline queue for offline send. Message storage—Core Data (iOS) or Room (Android) for offline access.
// iOS: offline message queue
class MessageQueue {
private let context: NSManagedObjectContext
private var syncTimer: Timer?
func sendMessage(_ content: String, conversationId: String) {
// Save locally immediately
let pending = PendingMessage(context: context)
pending.id = UUID().uuidString
pending.content = content
pending.conversationId = conversationId
pending.createdAt = Date()
pending.status = "PENDING"
try? context.save()
// Show in UI as sent
NotificationCenter.default.post(name: .messageSent, object: pending)
// Sync when connected
syncPendingMessages()
}
func syncPendingMessages() {
guard networkMonitor.isConnected else { return }
let pending = fetchPendingMessages()
pending.forEach { msg in
apiClient.sendMessage(msg) { result in
switch result {
case .success(let serverMessage):
msg.status = "DELIVERED"
msg.serverId = serverMessage.id
try? context.save()
case .failure:
// Retry on next sync
break
}
}
}
}
}
Ratings and Reputation
After closing order—mutual reviews. Rating affects search ranking. Protection from manipulation:
- Review only for specific completed order
- Mutual anonymity until publishing (like Upwork)—both see review only after both wrote
- Bayesian average instead of simple mean—new profile without reviews doesn't show "5.0 of 5"
Verification and KYC
For fund withdrawal—mandatory identity verification. Marketplaces often have two levels:
Basic—email + phone confirmation. Allows work but with withdrawal limit.
Extended—KYC via SDK (Sumsub, Veriff). Removes limits, gives "Verified" badge.
Timeline Estimates
| Scope | Timeline |
|---|---|
| Project catalog, profiles, bidding, no payments | 5–7 weeks |
| Escrow payments, Stripe Connect payouts | +3–4 weeks |
| Chat with WebSocket and offline-queue | +2–3 weeks |
| KYC integration, review system | +2 weeks |
Pricing is calculated individually after stack selection.







