Skip to content
Loading...

Case study

Building Marv.Inbox: a multi-tenant shared inbox for WhatsApp, Messenger and Instagram

A project about tenant isolation, channel integrations, the Meta review process, bilingual product UX, and the operational rules that keep a five-runtime system from going off the rails.

Next.js 16 inbox web app, bilingual LTR and RTL from the same code

NestJS backend with Prisma and per-tenant database resolution

BullMQ worker on Fly.io for channel sends, AI flows and scheduling

Socket.IO realtime service on Fly.io for presence and live surface state

Expo / React Native agent app on TestFlight with EAS OTA updates

WhatsApp Cloud API, Messenger and Instagram integrations through Meta App Review

The Starting Point

Marv.Inbox started from a specific gap. Small teams running customer conversations across WhatsApp, Messenger and Instagram did not have a real shared inbox for those channels. Most tools were CRMs with messaging bolted on, or chat widgets without a team workflow behind them.

The first version was a single Next.js app that could send and receive a WhatsApp message. Once it had to support multiple workspaces, agent handoff, and integrations that go through Meta's review process, one app was no longer enough.

Splitting The System

The architecture is split into five runtimes. A NestJS backend owns HTTP, auth, tenant resolution and job enqueueing. A BullMQ worker on Fly.io owns channel sends, AI flows, scheduling and long-running maintenance. A Socket.IO realtime service on Fly.io owns presence and live surface state. A Next.js web app is the inbox. An Expo agent app runs on iOS.

Splitting it this way kept each piece small enough to reason about. The backend stays fast and stateless. The worker handles retries and rate limits without blocking the API. The realtime service holds long-lived sockets on its own host. The web and mobile clients share contracts through a packages/types package, so a backend change cannot drift past either client silently.

Multi-tenant From Day One

Marv.Inbox is multi-tenant at the database level, not only by row-level filters. One control DB holds tenants, billing and platform admin data. Each tenant has its own Postgres database, resolved at request time. Prisma is used for both schemas, and the tenant context is available everywhere, including in the worker, where jobs carry their tenant slug end to end.

This cost more up front, but it kept downstream work simpler. Channel credentials, conversations, contacts, tasks and integrations live inside one tenant's database, so a bug in one query cannot leak data across workspaces. Production runs on Supabase. Local development runs on local Postgres with the same migrations.

Channels And Meta App Review

WhatsApp Cloud API was the first channel, and it sets the shape of the rest. There is a 24-hour customer service window, template messages for re-engagement, status callbacks per message, and webhook signatures that have to be verified on every request. The worker owns the outbound pipeline so the API does not have to wait on Meta, and the realtime service streams status transitions to whoever has the conversation open.

Messenger and Instagram added Meta App Review. That meant designing flows that satisfy Meta's HUMAN_AGENT handoff window (between 24 hours and 7 days), requesting Business Asset User Profile Access for profile name and avatar lookups, and writing review justifications that quote Meta's own policy text and point at the exact code path. It also meant building a reconnect flow that does not drop in-flight messages when an admin re-authorizes a page.

Agent UX In Two Languages

The inbox is bilingual from the same code. English LTR and Hebrew RTL are not two layouts. They are the same JSX reading from i18n and a direction-aware design system. Strings live in locale files, layout uses logical CSS properties, and every component is verified in both directions before it ships.

On top of that the inbox has optimistic message sending, typing and presence through Socket.IO, attachment handling, an internal team chat, a tasks surface, and a contacts view. The Expo mobile companion app reuses the same contracts and ships through TestFlight with EAS over-the-air updates.

Operating It

The platform runs across Cloud Run, Fly.io, Vercel, Supabase and Redis Cloud, with strict separation between local and production. Production credentials live only in production. Local seeds, simulations and tests target local infrastructure only.

There is a hard rule in the project that no database mutation runs without explicit approval, and that any code change touching routes, data flow or setup updates the matching CLAUDE.md docs in the same task. Those two rules have stopped more incidents than any single architectural choice.

Stack

Next.js, React, TypeScript, Tailwind, NestJS, Prisma, PostgreSQL, Redis, BullMQ, Socket.IO, Expo, React Native, WhatsApp Cloud API, Meta Graph API, OpenAI, Jest, Vitest, and Playwright.

← Back to portfolio