Nutralis CRM

Next.jsTypeScriptPostgreSQLElevenLabsPrisma

2026/Production/Full-stack & systems engineering

An internal CRM that is the single screen a phone-sales team lives in all day, fusing the webshop, the ERP, and the phone system into one workflow with an in-browser softphone and a live AI call assistant.

A tele-sales agent's job is simple to state and hard to do well: figure out who to call, call them, see their history, take the order or log why not, then move on. Doing that without one tool meant living in a stack of them: the WooCommerce admin for orders, the Metakocka ERP for where a parcel actually is, a desk phone with no record of calls, four separate messaging apps, a marketing tool, and a spreadsheet of who to ring next. There was no shared queue and a lot of copy-pasting phone numbers between windows.

None of those systems knew about each other, so the agent was the integration layer, holding the customer's state in their head across half a dozen tabs. The real constraint was not the UI, it was the runtime. A web request is short-lived and can be cold-started or torn down between calls, but a phone line, a PBX event feed, and a GSM SMS socket all need to stay open for hours. The system needed one screen where the next action is obvious, and a way to hold those long-lived connections without making the web app stateful.

I started with the data model and the systems-of-record map before any screens. The CRM owns its own Postgres for calls, reminders, conversations, leads, and the cached shipment status, and reads the live store through a separate read-only MariaDB mirror. Both run through Prisma with two adapters, so one ORM speaks to two engines and the boundary between the CRM's data and the shop's data stays explicit. The shape that fell out was a CRM that sits on top of the three external systems instead of replacing them, reading from all of them and writing back only where it owns the data.

From there I split out the work that does not belong in a request handler. Four single-purpose Bun services hold the connections the monolith cannot: the PBX event socket, the GSM SMS gateway's raw TCP feed, the new-order push, and the live-audio transcription bridge. That split also buys crash isolation, so a transcription leak can never take down the app the team is using to take orders. Everything that crosses a boundary is validated with Zod before it reaches a component.

The browser becomes the phone. A SIP and WebRTC softphone registers the agent's extension, one click dials a customer, and a blast dialer rings up to five people into one conference and connects the first to answer. Live calls are recorded, transcribed, and summarised through a pipeline to cloud storage, all driven by a state machine that turns raw PBX events into clean call records. The CRM computes the queues automatically, who to call and why, so the agent always opens onto the next action.

Beside the call sits a real-time AI assistant. The customer's audio streams as PCM to speech-to-text and a model, which surfaces short Slovenian reply suggestions to the agent mid-call, and an autonomous voice agent can place routine outbound calls on its own. One inbox unifies SMS, WhatsApp, Messenger, and Viber. In the background the ERP shipment status stays cached under an advisory lock, marketing data flows to Klaviyo, and the legal do-not-call checks run before cold calls. None of it blocks the agent.

The agent now runs an entire conversation without leaving the page. The work that used to be spread across a stack of separate tools is a single ordered queue, a click to dial, the customer's full history already in front of them, and an outcome captured in place, while the webshop, ERP, and marketing systems stay in sync behind the scenes.

It is the most ambitious system I have built, and it holds together because the architecture came first. The phone, the live AI help, the ERP sync, and four messaging channels feel like one product because each subsystem was designed around the daily workflow rather than bolted on around it. Every API response is validated at the boundary so backend drift surfaces as a clean error rather than a crash, React 19 with the compiler keeps the client code plain, and each part degrades gracefully instead of taking the others down with it.