All work

Microsoft Teams Platform — a second chat surface, then an abstraction under both

Second chat platform shipped in 3 months · a third was later built on the abstraction

The hard part of a second platform isn't the second platform. It's discovering how much of the first one was never really abstracted.

The problem

Rising Team ships a bot inside the customer's own workspace — 1:1 AI coaching you DM, plus a scheduled team-question bot that posts into a channel and collects threaded replies. It was Slack-only. Teams-first enterprises simply couldn't buy it.

The approach

I built the Teams surface end to end, then generalised what it taught me.

  • Auth, carefully. The pre-existing adapter had a test-environment flag that defaulted to enabled when unset — one missing environment variable between production and accepting unsigned inbound activities. The bypass now requires debug mode and a dev/test environment and no bearer token.
  • SSRF allowlist on a tenant-controlled URL. Teams hands you a per-tenant reply endpoint, and you POST to it. That's attacker-influenced input, so it's gated by a strict host allowlist — extracted into one module so install-time storage and send-time delivery can't drift apart. Tests cover suffix confusion, userinfo smuggling, Unicode case-folding, bracketed hosts and non-standard ports.
  • Streaming into a protocol that doesn't stream. Teams has no server-sent events, so a streamed LLM reply is posted once and then progressively rewritten, with flush thresholds tuned against the platform's update rate limit. Bridging a sync generator into an async handler needs a sentinel — StopIteration can't cross that boundary.
  • State without threads. A Teams 1:1 has one conversation id forever, so there's no natural session boundary. Session state rides on synthetic marker records that are excluded from the web chat history.
  • Cards that can't be disabled. Adaptive Cards have no disabled flag, so "expire this card" is implemented by replacing the posted message with a precomputed frozen snapshot.
  • Then the abstraction. A platform-client interface with frozen value objects that deliberately carry both Slack Block Kit and Teams Adaptive Card payloads rather than a lossy neutral format, plus an adapter registry with a boot-time completeness check — so a missing registration fails at startup instead of at the next scheduled send.

Stack

Python · Django · Microsoft Bot Framework · Microsoft Graph · MSAL · Adaptive Cards · Slack Bolt · Redis · RQ · PostgreSQL

Outcomes

  • Full Teams parity — webhook auth, Graph identity resolution, streaming 1:1 chat, scheduled channel posts with reply/edit/delete collection, card pickers and a home tab — shipped in roughly three months.
  • The adapter registry turned "add a third platform" from a hunt-and-edit across dispatch sites into a single registration. A third platform package now ships against it.
  • Sharing the stricter Teams validator across both platforms closed two real authorization defects on the older Slack path, including one where a forged action payload could surface coaching insights about an arbitrary user.

What I learned

  1. A second implementation is the cheapest audit you'll ever run. Every assumption Slack had baked in became visible the moment something else had to satisfy the same interface — including two security bugs that had been sitting in production.
  2. Don't build the neutral abstraction you want; build the honest one. Forcing Block Kit and Adaptive Cards through a shared "generic layout" would have lost both. Carrying both payloads and validating exactly-one is uglier on paper and correct in practice.
  3. Fail at boot, not at 9am. A registry that asserts completeness on startup converts a silent 3am scheduling failure into a deploy that refuses to start.