The Challenge
Telecom customers manage their phone systems through NetSapiens portals. Extensions, phone numbers, call routing — it's all there. When they wanted to add AI voice agents to handle calls, the obvious solution was to point them at a third-party AI voice platform. But those integrations connect via SIP trunking with 10-digit phone number transfers between platforms. Every extra hop degrades call quality, which degrades speech-to-text accuracy, which means the AI agent hears garbage and responds with garbage. The integration itself becomes the bottleneck.
Beyond the technical issues, customers don't want to context-switch between portals. They don't want separate logins for every vendor feature. They want to manage AI agents in the same place they manage everything else — the NetSapiens portal they're already logged into.
NetSapiens is a closed platform, but that doesn't mean you can't extend it. You can inject custom JavaScript and CSS via configuration URLs, embed interfaces as iframes, and exchange authentication tokens via PostMessage. It's not as simple as dropping in a third-party widget — you have to build the portal integration, the auth exchange, and the tenant isolation yourself. But the building blocks are there if you know where to look.
The Approach
The solution is a portal shim that injects into NetSapiens and embeds the AI platform as an iframe. When a customer logs into their NetSapiens portal, a custom JavaScript file loads, adds a navigation button, and creates an iframe pointed at the AI platform. The iframe runs a React SPA that handles all the AI agent management UI.
Authentication works via JWT exchange. NetSapiens includes a signed JWT in the portal's JavaScript context that contains the user's domain and role. The portal shim extracts that JWT and sends it to the AI platform via PostMessage. The platform validates the JWT signature, extracts the tenant domain claim, and issues its own session JWT. From that point forward, the iframe is authenticated and scoped to the correct tenant.
Tenant isolation uses PostgreSQL row-level security. Every database table includes a domain column. Every query runs within a tenant-scoped context that filters rows automatically. A user from companyA.com can only see agents, conversations, and recordings for companyA.com. The database enforces this at the row level, so even a SQL injection wouldn't leak cross-tenant data.
Portal Embedding Flow
Role-Based Access Control
The system supports three roles mapped from NetSapiens portal permissions:
- Viewers: Read-only access to agents, conversations, and recordings. Can't create or modify anything.
- Office Managers: Full CRUD on AI agents. Can create test calls, modify agent prompts, and view all conversations.
- Super Users: Admin panel access for all tenants. Can enable/disable tenant access and manage global settings.
The role comes from the scope claim in the NetSapiens JWT. The platform maps it to internal permissions and enforces them on every API request. An office manager from companyA.com can create agents for their domain but can't see or touch anything from companyB.com.
Architecture Diagram
Technical Stack
- Frontend: React SPA embedded via iframe, PostMessage for parent-child communication
- Backend: Fastify with tenant-scoped middleware, JWT validation via JWKS
- Database: PostgreSQL with row-level security policies on all multi-tenant tables
- Voice: Telnyx API for AI call handling, API keys stored server-side only
- Reverse Proxy: Caddy for automatic HTTPS and CSP headers
- Deployment: Docker Compose on Ubuntu, systemd for process management
The Outcome
Customers manage AI agents from the same portal they already use for everything else. No separate login, no vendor portal to learn. They click the AI Agents button in their NetSapiens navigation, the iframe loads, and they see their agents. Office managers create new agents, configure prompts, and test calls without leaving the portal.
Tenant isolation is enforced at the database layer. Every table with multi-tenant data includes a domain column and a row-level security policy. The application code sets the tenant context once per request, and PostgreSQL filters all queries automatically. A compromised API route can't leak cross-tenant data because the database won't return rows from other domains.
Telnyx handles the voice layer, but it's not a multi-tenant solution out of the box. One API key, one account — no concept of customer domains or tenant boundaries. We built the multi-tenant layer on our side: tenant-scoped API proxying, per-domain agent configuration, and server-side key management. The React app never sees the Telnyx API key, never stores it in localStorage, never includes it in network requests. That prevents key leakage via browser extensions, XSS attacks, or developer tools inspection.
The embedding strategy works within NetSapiens' constraints. You can't modify their portal directly, but you can inject JavaScript and CSS via configuration URLs. The portal shim is a single JavaScript file that adds a nav button and creates the iframe. The iframe communicates with the parent via PostMessage for authentication handshake and nothing else. The rest is a standard React SPA that runs independently inside the frame.