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 (auto-created) │ └─────────────────────────────────┘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 (auto-created during install via Microsoft Graph), 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 — full control. We never authenticate against your subscription except during the consent flow at install time (and only with the access token you explicitly granted).
- 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.