All work
Multi-Tenant SaaS / VoIP

LexerDial: Multi-Tenant Business Phone System

Architected a multi-tenant unified communications platform on carrier Call Control and WebRTC APIs, browser softphone, SMS/MMS inbox, IVR, call queues, voicemail, and recording.

Sector
Multi-Tenant SaaS / VoIP
Timeline
In active development
Role
Lead Systems Architect
  • Turborepo
  • Hono
  • Next.js 16
  • React 19
  • Drizzle ORM
  • PostgreSQL
  • WebRTC / SIP
  • React Native
LexerDial: Multi-Tenant Business Phone System
WebRTC → PSTN
Call Path

Browser softphone over JSON-RPC signaling, bridged to the public network

BYO carrier
Tenancy Model

Per-tenant API key and Call Control connection ID

Web + Mobile
Clients

Next.js dashboard and softphone, React Native/Expo app sharing one API

The problem

Business phone systems are sold as closed platforms: a tenant cannot bring their own carrier account, routing rules are configured through someone else's UI, and call data is trapped. Building an alternative means owning both the real-time telephony leg and the multi-tenant boundaries around it.

The solution

Built a Turborepo monorepo where every tenant connects their own carrier account and gets a full phone system, shared numbers, team calling, messaging, IVR menus, queues, voicemail with transcription, and recording, with all carrier access funneled through a single typed telephony package.

Approach

How it was built.

01

Wrap the Carrier, Always

A dedicated telephony package wraps the carrier SDK so the API server never calls the carrier directly. Every carrier interaction goes through typed helper functions, which makes the carrier a swappable dependency instead of a structural assumption spread across the codebase.

02

Webhook-Resolved Tenancy

Inbound calls arrive as carrier webhooks with no session context. The backend resolves which tenant owns the dialed number, evaluates that tenant's routing rules, and only then rings the assigned users, tenancy is established from the number, not from a request header.

03

Types as the Monorepo Contract

A shared package holds TypeScript types, Zod validation, and constants used by the Next.js dashboard, the Hono API, and the React Native client, so a routing-rule shape change breaks compilation everywhere it matters, at build time.

Technical detail

The parts worth explaining.

Two Signaling Paths, One Call

Outbound calls originate client-side over the WebRTC SDK's WebSocket signaling, while inbound calls originate server-side from Call Control webhooks. Both converge on the same call record, reconciling those two paths into one consistent state is the core of the call engine.

Number-Scoped Routing Rules

Routing rules are stored per number rather than per tenant, so a single tenant can run a sales queue, a support IVR, and a direct line off three numbers without conditional logic in the webhook handler.

What I took from it
  • Telephony state is genuinely distributed; treat carrier webhooks as the source of truth, not the client
  • Wrapping the vendor SDK on day one costs an afternoon and buys a decade of optionality
  • Monorepo type sharing is worth the tooling overhead the moment a mobile client enters the picture
Where it goes next
  • WebSocket presence layer for live agent and queue state
  • S3/R2 storage for recordings, voicemail, and MMS media
  • Billing and subscriptions ahead of the public SaaS launch

Got a problem shaped like this?

If something here looks like what you're facing, tell me about it. The first conversation is an architecture discussion, not a sales call.