Concepts
PointOfContactAI has three logically separate surfaces. Understanding the boundary between them is the difference between “I can debug this” and “I have no idea what’s happening”.
The three surfaces
┌────────────────────────────┐ ┌─────────────────────────────────┐│ ISV control plane │ │ Customer-stack runtime ││ (we operate this) │ │ (deployed into YOUR Azure) ││ │ │ ││ • /api/landing │ ◄─► │ • /api/chat (visitor) ││ • /api/managed-app/ │ │ • /api/agent/* (Teams agents) ││ notify │ │ • /api/dashboard ││ • /api/seat-report ◄─────┼─────┤ • /api/plan ││ • /api/deploy-status ◄───┼─────┤ • /api/teams-app.zip ││ • Marketplace fulfillment │ │ • Customer Cosmos/KV/SignalR/ ││ │ │ OpenAI (your subscription) │└────────────────────────────┘ └─────────────────────────────────┘ ▲ │ Teams SSO ▼ ┌─────────────────────────────────┐ │ Teams personal tab │ │ (your agents) │ │ │ │ • Pinned in your Teams tenant │ │ • Authenticates via YOUR own │ │ AAD app (in your tenant) │ └─────────────────────────────────┘ISV control plane
A small set of Azure Functions we operate at pocai-isv-cp.azurewebsites.net. Handles the marketplace billing lifecycle (when you buy, change plans, or cancel) and the orchestration of your install (the wizard backend).
What it stores: your subscription id, plan, seat quota, deployment state. What it does NOT store: any conversation content, user names, message bodies. The boundary is enforced by strict schemas + a CI lint that rejects log lines mentioning forbidden field names.
Customer-stack runtime
Everything that handles real customer data lives inside your Azure subscription in a resource group named after your stack:
- Cosmos DB — chat sessions, messages, token usage
- Azure OpenAI — the assistant
- SignalR Service — realtime visitor ↔ bot ↔ agent
- Key Vault — your admin key + AAD app secret
- Function App — runs the chat, dashboard, and agent endpoints
The Function App’s managed identity is the only identity with data-plane RBAC on these resources. We have zero standing access.
The customer-stack reports two content-free pings per hour to the ISV control plane:
- seat-report —
{subscriptionId, seatQuantity, timestamp}plus optional token usage aggregate - deploy-status —
{subscriptionId, deploymentId, deployedVersion, status, timestamp}
That’s it. Both are schema-validated at the ISV boundary; extra fields are rejected.
Teams personal tab
A standard Teams app sideloaded into your tenant. Your agents see it in their left rail. The app’s webApplicationInfo.id points at an AAD app in your tenant (created by you during Teams app setup), so SSO tokens go straight from Teams to your customer-stack’s Function App without ever crossing the ISV’s tenant.
Three trust boundaries
- You ↔ Marketplace — billing relationship. Microsoft handles payment; we receive notifications.
- You ↔ Your Azure — you own the subscription and every resource. Our only foothold is publisher Contributor on the Managed Resource Group — control-plane only, no data-plane access, fully visible in your Activity Log.
- Your Azure ↔ ISV control plane — content-free callbacks only. Schema-enforced, both sides.
Sessions, claims, and seats
A “session” is one conversation initiated by a visitor on your website.
When a visitor opens the widget and sends a message, the bot (Azure OpenAI) replies. The session is open.
If the visitor types something the bot can’t handle — or your bot’s system prompt is configured to escalate proactively — the session becomes human_requested. A red badge appears in the Teams tab; an agent claims it. Now the session is claimed (by that agent’s AAD object id) and future visitor messages route to the agent via SignalR, bypassing the bot. The agent replies via the Teams tab; the visitor sees the reply in their widget.
The agent closes the session when done. The seat is freed.
A “seat” = a distinct agent oid that has claimed a session in the last hour. Your plan’s quota is the maximum concurrent agents. The dashboard shows live seats used / seat quota and a grace banner if you exceed.