The anatomy (what a .zip actually declares)
The app package = manifest.json + icons. The manifest names: the app id, the developer, and its CAPABILITIES —
| Capability | What it really is | Data path |
|---|---|---|
| Tab | A pinned webpage (SSO-capable iframe) | User browser -> the tab's own web host |
| Bot | A registered Azure Bot endpoint | Teams -> Bot Framework -> developer's service |
| Message extension | Bot-powered search/actions from the compose box | Same as bot |
| Webhook/connector | Inbound: card-post URL; the legacy connector class retired 2026 (Workflows replaced it) | Poster -> Teams |
| Meeting app | Tab/bot living inside meetings | As above, meeting-scoped |
Plus the permission block: Graph scopes (delegated/application) and RSC grants (team/chat-scoped powers granted by owners). Review = manifest read + that block: an app is exactly its endpoints plus its scopes.
The lifecycle for custom (line-of-business) apps
Developer Portal (or CI) builds the package -> upload to the tenant catalog (the org's private store) -> the SAME three governance layers apply as for store apps. Sideloading ("upload a custom app") is the dev-loop path — scoped to developers by setup policy, off for everyone else in prod. Updates: new manifest version to the catalog; pinned deployments follow.
What to watch (proofs)
- The blast radius review: the manifest's validDomains, bot id and permission/ RSC block — a five-minute read that IS the security review's core artifact.
- Where custom apps are: TAC Manage apps filtered to "Custom" — the org's real LOB inventory, with install counts.
- Who can sideload: the setup-policy census (
Get-CsTeamsAppSetupPolicy | fl AllowSideLoadingequivalents / upload settings) — non-dev populations should read false, and the scan's drift alert covers the day they don't. - Runtime behaviour: bot traffic goes to the registered endpoint — the app's own service logs (yours, for LOB apps) joined with Teams-side install data close the loop on "is anyone using this".