Distributed Messaging System
99% infra cost cut across 3 PadiUMKM products
Problem
PadiUMKM runs three separate products: a marketplace, a POS app, and a partner portal, each of which needed real-time chat. Every team had either built their own thin wrapper around a third-party service or was about to. The result was three separate bills, three integration surfaces, and three failure modes. Monthly messaging costs were climbing.
Each product was also directly coupled to an external vendor's SDK. Any pricing change or API deprecation would touch all three codebases at once.
Role
I owned the system from scratch: architecture decisions, backend implementation (Node.js on Firebase), frontend SDK design, and the rollout across all three products. There was no dedicated infrastructure team, so I also handled deployment and observability.
Approach
Shared platform over per-product integrations
Rather than each product owning its own messaging stack, I built a single internal messaging service they all connect to. The vendor dependency moved to one place: change the service once, and the three products don't notice.
Firebase as the transport layer
Firebase Realtime Database gave us WebSocket-level latency without running a WebSocket server. The key decision was treating Firebase as a dumb transport: business logic (room creation, membership, permissions) lives in the Node.js service, not in Firebase rules. That kept the domain model portable.
Feature parity with zero lock-in
The internal API was designed to be vendor-neutral:
createRoom(participants, metadata)— not Firebase-specificsendMessage(roomId, payload)— wraps a Firebase write, but the caller doesn't know thatsubscribeToRoom(roomId, callback)— returns an unsubscribe function, not a Firebase ref
Swapping Firebase for another transport later would leave the three product codebases unchanged.
Rollout strategy
I rolled out one product at a time, starting with the lowest-traffic one (the partner portal). Each rollout was a straight cutover with no feature flag, because the API surface matched what products were already calling. That let us catch integration issues early with minimal user impact.
Technical Decisions
Why not a managed service (e.g. Stream, Sendbird)?
Managed chat APIs charge per MAU or per message. At the usage levels we projected across three products, the monthly cost was significant and would grow with the user base. Building on Firebase, which we were already paying for, reduced that cost to near zero.
Why Node.js over the Firebase Admin SDK directly in each product?
Embedding Admin SDK logic in three frontends would have put service account credentials in client bundles. A thin Node.js intermediary keeps credentials server-side and gives us one place to enforce business rules.
Why Realtime Database over Firestore?
Chat needs ordered message streams and low-latency fan-out. Realtime Database's ordered lists and listener model fit that directly. Firestore's query model suits structured data at rest better than real-time event streams.
Outcome
- −99% infrastructure cost compared to the per-product approach
- High uptime across all three products throughout the rollout
- Single integration surface for all future products in the ecosystem
- The public case study and open-source repo (
distributed-chat-platform-case-study) document the architecture for others building similar systems