<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Het</title>
    <description>The latest articles on DEV Community by Het (@het101).</description>
    <link>https://dev.to/het101</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1069959%2Fa3fcacde-0540-4b79-9849-5e37ce20687d.jpg</url>
      <title>DEV Community: Het</title>
      <link>https://dev.to/het101</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/het101"/>
    <language>en</language>
    <item>
      <title>Azure Communication Services Chat is retiring - and your chat history goes with it</title>
      <dc:creator>Het</dc:creator>
      <pubDate>Sun, 27 Sep 2026 17:06:08 +0000</pubDate>
      <link>https://dev.to/het101/azure-communication-services-chat-is-retiring-and-your-chat-history-goes-with-it-3ioe</link>
      <guid>https://dev.to/het101/azure-communication-services-chat-is-retiring-and-your-chat-history-goes-with-it-3ioe</guid>
      <description>&lt;p&gt;Microsoft announced in September 2026 that &lt;strong&gt;Azure Communication Services (ACS) Chat retires on 30 September 2028&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There is already decent coverage of the announcement itself, which services are affected, what the replacement options are, and how to map your dependencies.&lt;/p&gt;

&lt;p&gt;This post is about the part I haven't seen enough people talk about, and it happens to be the part with a hard deadline attached:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What happens to your messages?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your application has been using ACS Chat as its conversation store, this is the migration problem you need to start thinking about now.&lt;/p&gt;




&lt;h2&gt;
  
  
  The sentence to read twice
&lt;/h2&gt;

&lt;p&gt;Microsoft's &lt;a href="https://learn.microsoft.com/en-us/azure/communication-services/acs-retirement-and-breaking-changes-guide" rel="noopener noreferrer"&gt;retirement and breaking changes guide&lt;/a&gt;, which describes itself as the authoritative source, says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After the retirement date, the supporting data and telemetry for retired and standalone services will be decommissioned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And, more specifically:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ACS will maintain chat history according to your storage policies until the retirement date of September 30, 2028.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read that again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your chat history is not migrated for you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It doesn't sit somewhere waiting for you to collect it after the service disappears.&lt;/p&gt;

&lt;p&gt;If those conversations matter, support threads, care conversations, customer communications, audit history, anything you may be asked to produce two years from now, &lt;strong&gt;they need to leave ACS before ACS Chat does.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is also a nearer date worth knowing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;23 October 2026:&lt;/strong&gt; new customers can no longer sign up for the retiring services.&lt;/p&gt;

&lt;p&gt;Existing resources continue working through the transition, so if you are already using ACS Chat, that date doesn't lock you out of your existing estate.&lt;/p&gt;

&lt;p&gt;It simply closes the door behind you.&lt;/p&gt;




&lt;h2&gt;
  
  
  The official path and who it is for
&lt;/h2&gt;

&lt;p&gt;Microsoft's stated replacement is the &lt;strong&gt;Microsoft Graph Chat APIs&lt;/strong&gt;, which the retirement guide lists as requiring a &lt;strong&gt;Teams license&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a large set of customers, that is probably the intended migration path.&lt;/p&gt;

&lt;p&gt;If your chat is between people who already have Microsoft 365 identities, and you're comfortable with Teams owning the conversation experience and governance model, the official path may fit your workload.&lt;/p&gt;

&lt;p&gt;This post isn't an argument against it.&lt;/p&gt;

&lt;p&gt;The problem is that not every ACS Chat workload looks like Teams.&lt;/p&gt;

&lt;p&gt;And one thing is worth saying explicitly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Microsoft will not migrate your ACS chat history into Teams for you.&lt;/strong&gt; Graph does have a message import API, but running it is your project: extracting from ACS, mapping every author to an Entra identity, transforming content, reconciling counts. The tooling exists; the work is yours.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction matters because the difficult part isn't simply putting messages into another API.&lt;/p&gt;

&lt;p&gt;It's preserving the meaning of those messages while moving between two different identity and conversation models.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who it is &lt;em&gt;not&lt;/em&gt; for
&lt;/h2&gt;

&lt;p&gt;Microsoft's retirement guide is unusually direct about this.&lt;/p&gt;

&lt;p&gt;It says workloads requiring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;anonymous or application-defined identities&lt;/li&gt;
&lt;li&gt;a white-label embedded client&lt;/li&gt;
&lt;li&gt;consumer-scale chat outside Teams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;should evaluate other communication providers or redesign the experience.&lt;/p&gt;

&lt;p&gt;If you recognise your product in that description, you're in the group with the most architectural work and the least prescriptive migration guidance.&lt;/p&gt;

&lt;p&gt;Let's make those requirements concrete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application-defined identities
&lt;/h3&gt;

&lt;p&gt;ACS allows your application to create communication users who aren't people in your Microsoft Entra ID.&lt;/p&gt;

&lt;p&gt;Think:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;patients&lt;/li&gt;
&lt;li&gt;customers&lt;/li&gt;
&lt;li&gt;drivers&lt;/li&gt;
&lt;li&gt;marketplace users&lt;/li&gt;
&lt;li&gt;external participants&lt;/li&gt;
&lt;li&gt;application-specific accounts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no Entra identity to map them to.&lt;/p&gt;

&lt;p&gt;They were never supposed to have one.&lt;/p&gt;

&lt;h3&gt;
  
  
  White-label embedded chat
&lt;/h3&gt;

&lt;p&gt;Your product, your branding, your auth, your workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Send them to Teams" isn't an option you can offer.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Consumer-scale chat
&lt;/h3&gt;

&lt;p&gt;You may have thousands or millions of users.&lt;/p&gt;

&lt;p&gt;Giving every participant a Microsoft 365 license isn't practical, nor is Teams necessarily the product experience you want to expose.&lt;/p&gt;

&lt;p&gt;For these workloads, the &lt;strong&gt;destination is an open architectural decision&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;extraction is not&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Whatever you eventually choose, your messages have to come out of ACS first.&lt;/p&gt;

&lt;p&gt;And that extraction problem is essentially the same regardless of where you eventually put the data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why getting the data out is harder than it sounds
&lt;/h2&gt;

&lt;p&gt;This is the part I want to be useful about.&lt;/p&gt;

&lt;p&gt;I know because I did a smaller, unplanned version of this migration last year.&lt;/p&gt;

&lt;p&gt;And I got most of it wrong the first time.&lt;/p&gt;

&lt;p&gt;Here are the things that caught me.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. There is no export button
&lt;/h3&gt;

&lt;p&gt;This sounds obvious, but it changes the entire migration strategy.&lt;/p&gt;

&lt;p&gt;There isn't a convenient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Export all my ACS Chat history
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;API.&lt;/p&gt;

&lt;p&gt;There is no bulk export operation that gives you your entire chat estate in one shot.&lt;/p&gt;

&lt;p&gt;Instead, you need to walk the ACS REST APIs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resource
   ↓
Identity
   ↓
Chat Threads
   ↓
Messages
   ↓
Participants
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you need to do that &lt;strong&gt;thread by thread and page by page&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means your migration process needs to deal with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pagination&lt;/li&gt;
&lt;li&gt;throttling&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;transient failures&lt;/li&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;checkpointing&lt;/li&gt;
&lt;li&gt;resumability&lt;/li&gt;
&lt;li&gt;partial migrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a one-off script you run from your laptop and forget about.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. ACS identities are scoped to the resource that created them
&lt;/h3&gt;

&lt;p&gt;This is the one that bites hardest.&lt;/p&gt;

&lt;p&gt;An ACS identity looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;8:acs:&amp;lt;resourceGuid&amp;gt;_&amp;lt;userGuid&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the resource GUID.&lt;/p&gt;

&lt;p&gt;It's part of the identity.&lt;/p&gt;

&lt;p&gt;That means an ACS identity isn't globally portable.&lt;/p&gt;

&lt;p&gt;It is tied to the ACS resource that created it.&lt;/p&gt;

&lt;p&gt;And that creates a nasty data-model problem.&lt;/p&gt;

&lt;p&gt;Suppose your application has something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users
-------------------------
id
name
acs_identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your messages reference that ACS identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;messages
-------------------------
id
thread_id
sender_acs_identity
content
created_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That feels perfectly reasonable while ACS is your chat platform.&lt;/p&gt;

&lt;p&gt;But once the ACS resource disappears, those identities disappear with it.&lt;/p&gt;

&lt;p&gt;Your application-level relationship between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your User ID
     ↓
ACS identity
     ↓
Message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is no longer a durable identity model.&lt;/p&gt;

&lt;h4&gt;
  
  
  I learned this the hard way
&lt;/h4&gt;

&lt;p&gt;During one resource migration I replayed &lt;strong&gt;7,200 threads&lt;/strong&gt;. The messages didn't disappear. Something worse happened. Of the messages in those threads, &lt;strong&gt;7,022 displayed the wrong author&lt;/strong&gt;, every one showing the system user as the sender.&lt;/p&gt;

&lt;p&gt;Every replayed thread also rejected replies with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Forbidden
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the same time, &lt;strong&gt;649 stored ACS identities belonged to the dead resource&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Nobody lost a message.&lt;/p&gt;

&lt;p&gt;The important part is &lt;em&gt;why&lt;/em&gt; we were able to recover from it.&lt;/p&gt;

&lt;p&gt;The original extraction had accidentally preserved the &lt;strong&gt;sender IDs and participant lists&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That meant we still had enough information to reconstruct the original authorship and participant relationships.&lt;/p&gt;

&lt;p&gt;I had preserved those fields because they happened to be in the extraction.&lt;/p&gt;

&lt;p&gt;Not because I had designed the migration around them.&lt;/p&gt;

&lt;p&gt;That was luck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't build your migration on luck.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Preserve the sender IDs and participant lists deliberately.&lt;/p&gt;

&lt;p&gt;The lesson wasn't that the messages were safe.&lt;/p&gt;

&lt;p&gt;The lesson was that &lt;strong&gt;the message body alone isn't the history.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The identity relationships around the message are part of the history too.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. You can only list threads an identity participates in
&lt;/h3&gt;

&lt;p&gt;Another problem is discovering the complete set of conversations.&lt;/p&gt;

&lt;p&gt;There isn't an administrative:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;listAllChatThreads()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;operation that simply gives you everything.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;listChatThreads&lt;/code&gt; operates in the context of the identity whose token you're using.&lt;/p&gt;

&lt;p&gt;That means extraction becomes a discovery problem.&lt;/p&gt;

&lt;p&gt;You need to know:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Who can see which threads?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your system identity was added to every conversation, you're in relatively good shape.&lt;/p&gt;

&lt;p&gt;If it wasn't, things get more interesting.&lt;/p&gt;

&lt;p&gt;Some threads may only be reachable through individual participants.&lt;/p&gt;

&lt;p&gt;And if your application has lost track of those participants, you may have conversations that are difficult, or potentially impossible, to discover through your normal application flow.&lt;/p&gt;

&lt;p&gt;This is why &lt;strong&gt;inventorying the estate should be the first step&lt;/strong&gt;, not writing the migration script.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Large migrations are long enough to fail
&lt;/h3&gt;

&lt;p&gt;Let's say you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;thousands of threads&lt;/li&gt;
&lt;li&gt;tens of thousands of messages&lt;/li&gt;
&lt;li&gt;multiple participants&lt;/li&gt;
&lt;li&gt;API throttling&lt;/li&gt;
&lt;li&gt;network failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your migration isn't going to be instantaneous.&lt;/p&gt;

&lt;p&gt;And if your extraction script looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for thread in threads:
    download(thread)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you're going to have a bad day.&lt;/p&gt;

&lt;p&gt;You need something closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;discover
   ↓
checkpoint
   ↓
extract
   ↓
validate
   ↓
checkpoint
   ↓
retry failures
   ↓
resume
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At minimum, build for:&lt;/p&gt;

&lt;h4&gt;
  
  
  Pagination
&lt;/h4&gt;

&lt;p&gt;Never assume one API response contains everything.&lt;/p&gt;

&lt;h4&gt;
  
  
  Retry with backoff
&lt;/h4&gt;

&lt;p&gt;Expect throttling and transient failures.&lt;/p&gt;

&lt;h4&gt;
  
  
  Checkpointing
&lt;/h4&gt;

&lt;p&gt;Record what has already been extracted.&lt;/p&gt;

&lt;h4&gt;
  
  
  Resumability
&lt;/h4&gt;

&lt;p&gt;If the process dies after 4,000 threads, you should restart from approximately 4,000, not from zero.&lt;/p&gt;

&lt;h4&gt;
  
  
  Idempotency
&lt;/h4&gt;

&lt;p&gt;Running the migration again shouldn't duplicate everything.&lt;/p&gt;

&lt;p&gt;A migration tool that can resume safely is considerably more valuable than one that is fast when everything goes perfectly.&lt;/p&gt;

&lt;p&gt;Because everything rarely goes perfectly.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Timestamps and authorship don't automatically survive a migration
&lt;/h3&gt;

&lt;p&gt;This is another easy trap.&lt;/p&gt;

&lt;p&gt;Suppose you extract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sender"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8:acs:..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-14T10:23:11Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then replay that message into another chat system.&lt;/p&gt;

&lt;p&gt;The destination system will generally know:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;createdAt = NOW()
sender = whoever performed the API request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;createdAt = 2026-02-14T10:23:11Z
sender = original participant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If historical fidelity matters, you need to preserve those values explicitly.&lt;/p&gt;

&lt;p&gt;For example, your durable message model might look more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"conversationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"originalCreatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-14T10:23:11Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"azure-communication-services"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj6dpxaapt7k9iqpxzajq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj6dpxaapt7k9iqpxzajq.png" alt="Data model mapping: nine fields on an ACS source message mapped to their&lt;br&gt;
equivalents in your own archive. id stays id; threadId becomes conversationId;&lt;br&gt;
sender, an 8:acs: identity, becomes authorId holding your own user ID; content&lt;br&gt;
becomes body; createdAt becomes originalCreatedAt; participants, also 8:acs:&lt;br&gt;
identities, become participants holding your user IDs; messageType and&lt;br&gt;
attachments are unchanged; and metadata becomes sourceMetadata, keeping the ACS&lt;br&gt;
identifiers as provenance rather than as the primary key.&lt;br&gt;
" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
The exact schema is up to you.&lt;/p&gt;

&lt;p&gt;The important thing is deciding &lt;strong&gt;before migration&lt;/strong&gt; which fields represent historical truth.&lt;/p&gt;

&lt;p&gt;Otherwise you can successfully migrate every message and still lose the two attributes that make the conversation understandable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;who said it and when they said it.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  A plan that works backwards from the deadline
&lt;/h2&gt;

&lt;p&gt;The temptation is to start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Which chat provider are we moving to?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I think that's backwards.&lt;/p&gt;

&lt;p&gt;Start with the data.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Find out what you actually have
&lt;/h3&gt;

&lt;p&gt;Before choosing a destination, build an inventory.&lt;/p&gt;

&lt;p&gt;At minimum, determine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total threads
Total messages
Total participants
Active identities
Stored identities
Stale identities
Threads per identity
Threads without system identity
Messages per thread
Oldest message
Newest message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may be surprised by the numbers.&lt;/p&gt;

&lt;p&gt;Especially if ACS has been running for several years.&lt;/p&gt;

&lt;p&gt;You also want to identify conversations where your application no longer has a usable participant identity.&lt;/p&gt;

&lt;p&gt;Those are the conversations you don't want to discover six months before retirement.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Get the history into a store you own
&lt;/h3&gt;

&lt;p&gt;This is the most important step.&lt;/p&gt;

&lt;p&gt;Don't make your future chat provider your archive.&lt;/p&gt;

&lt;p&gt;Create a durable copy of your history in infrastructure that &lt;strong&gt;you control&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And don't key that archive primarily around ACS identities.&lt;/p&gt;

&lt;p&gt;Use your application's own identifiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your User ID
       ↓
Conversation ID
       ↓
Message ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the ACS identifiers as source metadata where useful, but don't make them the foundation of your long-term identity model.&lt;/p&gt;

&lt;p&gt;Preserve the original:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;message ID&lt;/li&gt;
&lt;li&gt;conversation/thread ID&lt;/li&gt;
&lt;li&gt;sender ID&lt;/li&gt;
&lt;li&gt;participant list&lt;/li&gt;
&lt;li&gt;message body&lt;/li&gt;
&lt;li&gt;original timestamp&lt;/li&gt;
&lt;li&gt;message type&lt;/li&gt;
&lt;li&gt;attachments/metadata where applicable&lt;/li&gt;
&lt;li&gt;relevant source identifiers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact fields depend on your application and retention requirements.&lt;/p&gt;

&lt;p&gt;Once the history is safely in your own datastore, the retirement deadline changes character.&lt;/p&gt;

&lt;p&gt;It stops being primarily a &lt;strong&gt;data-loss risk&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It becomes an ordinary migration project.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3: Decide where new conversations belong
&lt;/h3&gt;

&lt;p&gt;Only now should you make the destination decision.&lt;/p&gt;

&lt;p&gt;Depending on your requirements, that might be:&lt;/p&gt;

&lt;h4&gt;
  
  
  Microsoft Graph / Teams
&lt;/h4&gt;

&lt;p&gt;If your users and product model fit Microsoft's Teams-based direction.&lt;/p&gt;

&lt;h4&gt;
  
  
  Another communication provider
&lt;/h4&gt;

&lt;p&gt;If you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application-defined identities&lt;/li&gt;
&lt;li&gt;embedded chat&lt;/li&gt;
&lt;li&gt;white-label UI&lt;/li&gt;
&lt;li&gt;consumer-scale conversations&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Your own chat system
&lt;/h4&gt;

&lt;p&gt;If chat is a core product capability and you need complete control over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identity&lt;/li&gt;
&lt;li&gt;storage&lt;/li&gt;
&lt;li&gt;UI&lt;/li&gt;
&lt;li&gt;retention&lt;/li&gt;
&lt;li&gt;search&lt;/li&gt;
&lt;li&gt;moderation&lt;/li&gt;
&lt;li&gt;auditability&lt;/li&gt;
&lt;li&gt;integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important thing is that &lt;strong&gt;the archive shouldn't depend on which one you eventually choose.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 4: Verify
&lt;/h3&gt;

&lt;p&gt;Don't finish when the migration script says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Migration complete: 100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finish when the data has been verified.&lt;/p&gt;

&lt;p&gt;At minimum, compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ACS                    Destination/archive

Thread count      ==   Thread count
Message count     ==   Message count
Participant count ==   Participant count
Author mapping    ==   Author mapping
Original dates    ==   Original dates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And don't only verify totals.&lt;/p&gt;

&lt;p&gt;Pick real conversations and inspect them.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Conversation A
  37 messages
  4 participants

Conversation B
  12 messages
  2 participants

Conversation C
  184 messages
  7 participants
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;first message&lt;/li&gt;
&lt;li&gt;last message&lt;/li&gt;
&lt;li&gt;authorship&lt;/li&gt;
&lt;li&gt;ordering&lt;/li&gt;
&lt;li&gt;timestamps&lt;/li&gt;
&lt;li&gt;participants&lt;/li&gt;
&lt;li&gt;attachments&lt;/li&gt;
&lt;li&gt;message metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A migration that &lt;strong&gt;finished&lt;/strong&gt; isn't necessarily a migration that is &lt;strong&gt;correct&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The migration is easier to reason about if you separate &lt;strong&gt;extraction&lt;/strong&gt; from &lt;strong&gt;destination&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffuca0voxx6n90bxhox20.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffuca0voxx6n90bxhox20.png" alt="Migration architecture: a four-stage left-to-right flow. Azure Communication&lt;br&gt;
Services, holding chat threads, messages and participants, is extracted into an&lt;br&gt;
extraction layer handling discovery, pagination, retry with backoff,&lt;br&gt;
checkpointing and identity mapping. That normalises and stores into your own&lt;br&gt;
store, holding your user IDs, original timestamps, message history and ACS&lt;br&gt;
metadata. Only later does it fan out to a destination: Microsoft Teams via&lt;br&gt;
Graph, another communication provider, or your own chat system.&lt;br&gt;
" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The middle box is deliberately generic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Threadvault is one implementation of that extraction layer; a script you write yourself is another.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The important architectural boundary is that the extraction layer gets the history into a durable format you control &lt;strong&gt;before&lt;/strong&gt; the destination becomes part of the problem.&lt;/p&gt;


&lt;h2&gt;
  
  
  So I built a tool for it
&lt;/h2&gt;

&lt;p&gt;I wrote the recovery tooling for my own incident afterwards.&lt;/p&gt;

&lt;p&gt;Then I turned it into something reusable, because writing migration tooling under pressure at 2 AM was miserable, and entirely avoidable.&lt;/p&gt;

&lt;p&gt;It's called &lt;strong&gt;Threadvault&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Threadvault is an open-source CLI for extracting and preserving Azure Communication Services Chat history.&lt;/p&gt;

&lt;p&gt;It's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open source&lt;/li&gt;
&lt;li&gt;Apache-2.0 licensed&lt;/li&gt;
&lt;li&gt;Free&lt;/li&gt;
&lt;li&gt;No paid tier&lt;/li&gt;
&lt;li&gt;No account required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And to be completely clear:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Threadvault does not migrate your history into Teams.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is focused on the extraction and preservation problem.&lt;/p&gt;

&lt;p&gt;The goal is much simpler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Get your data out of ACS while you still can.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Start with &lt;code&gt;doctor&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The first command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx threadvault doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;doctor&lt;/code&gt; is deliberately &lt;strong&gt;read-only&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It runs in seconds, &lt;strong&gt;never reads a message body&lt;/strong&gt;, audits &lt;strong&gt;five failure modes&lt;/strong&gt;, and gives you a number you can use to understand the state of your ACS Chat environment before attempting a migration.&lt;/p&gt;

&lt;p&gt;The point is to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What does my ACS estate actually look like?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;before you start moving anything.&lt;/p&gt;




&lt;h2&gt;
  
  
  One more reason to run it against real infrastructure
&lt;/h2&gt;

&lt;p&gt;I have &lt;strong&gt;215 tests and 79% code coverage&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That wasn't enough.&lt;/p&gt;

&lt;p&gt;Running the tool against real infrastructure found &lt;strong&gt;ten defects that the test suite didn't catch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's not a claim that the tool is perfect.&lt;/p&gt;

&lt;p&gt;It's the opposite.&lt;/p&gt;

&lt;p&gt;It is a reminder that API behavior, real identity graphs, pagination, stale resources, permissions, and production data tend to expose failure modes that unit tests don't.&lt;/p&gt;

&lt;p&gt;That's also why I would strongly recommend running any extraction tooling against your actual ACS estate early, not just against mocked data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Don't wait until 2028
&lt;/h2&gt;

&lt;p&gt;30 September 2028 sounds far away.&lt;/p&gt;

&lt;p&gt;For a production chat estate, it isn't.&lt;/p&gt;

&lt;p&gt;The migration isn't just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ACS → New Chat Provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Discover
   ↓
Inventory
   ↓
Extract
   ↓
Preserve identities
   ↓
Preserve timestamps
   ↓
Validate
   ↓
Archive
   ↓
Choose destination
   ↓
Migrate active experience
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the first half of that work is independent of whatever your eventual destination becomes.&lt;/p&gt;

&lt;p&gt;That's the useful thing about starting early.&lt;/p&gt;

&lt;p&gt;You don't have to know today whether you're moving to Teams, another provider, or something you build yourself.&lt;/p&gt;

&lt;p&gt;You only need to make sure that &lt;strong&gt;your historical conversations don't disappear with the service that currently holds them.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you're running ACS Chat today, I would ask four questions now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;How many threads and messages do we actually have?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can we reach every conversation through the identities we still control?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Are our application-level user IDs mapped independently from ACS identities?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do we have a verified copy of the history in infrastructure we own?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the answer to #4 is no, that's probably the work to start with.&lt;/p&gt;

&lt;p&gt;The destination can come later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data has a deadline.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/communication-services/acs-retirement-and-breaking-changes-guide" rel="noopener noreferrer"&gt;Microsoft ACS Retirement and Breaking Changes Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Het101/threadvault" rel="noopener noreferrer"&gt;Threadvault on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://het101.github.io/threadvault" rel="noopener noreferrer"&gt;Threadvault documentation, runbook, and post-mortem&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Dates and quotations accurate as of 27 September 2026. Microsoft's retirement guide is the authoritative source and may change.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you're dealing with an ACS Chat estate and have run into identity, pagination, discovery, or migration problems, I'd be interested in hearing what you've found.&lt;/p&gt;

&lt;p&gt;There are probably more edge cases hiding in production systems than any migration guide will mention.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>architecture</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
