<?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: StateKeep</title>
    <description>The latest articles on DEV Community by StateKeep (@statekeep).</description>
    <link>https://dev.to/statekeep</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3944368%2F33f903c2-5aa2-4919-90be-94dbc326c4f6.png</url>
      <title>DEV Community: StateKeep</title>
      <link>https://dev.to/statekeep</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/statekeep"/>
    <language>en</language>
    <item>
      <title>The XState persistence problem is five years old. Here is what we built to finally solve it.</title>
      <dc:creator>StateKeep</dc:creator>
      <pubDate>Mon, 25 May 2026 03:41:27 +0000</pubDate>
      <link>https://dev.to/statekeep/the-xstate-persistence-problem-is-five-years-old-here-is-what-we-built-to-finally-solve-it-39af</link>
      <guid>https://dev.to/statekeep/the-xstate-persistence-problem-is-five-years-old-here-is-what-we-built-to-finally-solve-it-39af</guid>
      <description>&lt;p&gt;In 2019 someone opened a GitHub issue in the XState repository. The title was "&lt;em&gt;How do I persist XState actor state between server restarts?&lt;/em&gt;" It became one of the most-upvoted open issues in the repo. The answer from the core team was honest: XState doesn't handle persistence. Serialize the state object and store it yourself.&lt;br&gt;
Five years later, that answer hasn't changed. Dozens of blog posts exist showing developers how to roll their own persistence layer. Every single one of them is a developer reinventing the same infrastructure that has nothing to do with their actual product.&lt;br&gt;
We got tired of reinventing it. So we built StateKeep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem with rolling your own&lt;/strong&gt;&lt;br&gt;
Persisting XState state sounds straightforward. Call &lt;code&gt;actor.getSnapshot()&lt;/code&gt;, serialize it, store it in Redis or Postgres. On startup, rehydrate. Done.&lt;br&gt;
It works. Until it doesn't.&lt;br&gt;
XState v5 shipped in 2023 and changed the snapshot format. Teams with actors persisted in the old format had a bad week. Some serialized state just crashed on deserialization. Others silently corrupted context. The format was never treated as a public API — because XState is a library, and libraries are not responsible for what you do with their output.&lt;br&gt;
Even before the v5 breakage, there was the migration problem. Your order management workflow lives in a table. You have 40,000 active orders. Your product team needs to add a quality review step. You write a migration script. You test it in staging. You run it in production at midnight. You discover that 847 orders were in an edge state you didn't account for. You spend the next three hours fixing them manually.&lt;br&gt;
This is not a XState problem. It is not a developer competence problem. It is an infrastructure gap. There is no layer between "XState the library" and "your application database" that takes responsibility for keeping actors alive through code changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Stately built — and where it ends&lt;/strong&gt;&lt;br&gt;
Stately saw this problem. They built Stately Cloud: a hosted service that persists your XState actors, keeps them alive between restarts, and gives you an API to send events and read state.&lt;br&gt;
It is a real solution for the right use case. If you are building a side project, you are a JavaScript shop, and your data can live on their servers — Stately Cloud is worth evaluating.&lt;br&gt;
Three things make it a hard no for a large chunk of teams:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data residency&lt;/strong&gt;. Your actor context contains your customer's data. For any team in fintech, healthcare, insurance, or enterprise SaaS with compliance requirements — sending that data to a third-party hosted service is often not an option. Stately Cloud has no self-hosted deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language lock-in&lt;/strong&gt;. Stately Cloud requires XState. Not "XState-compatible JSON" — actual XState TypeScript. If your backend is Python, Go, Java, or anything other than JavaScript, you are locked out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No path-based migration&lt;/strong&gt;. When you update a workflow definition, Stately Cloud does not help you decide which of your 40,000 in-flight actors should move to the new version and where they should land. You write that logic yourself.
That third one is the one we spent the most time on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The migration problem is harder than it looks&lt;/strong&gt;&lt;br&gt;
Here is a scenario that sounds simple but breaks every migration tool I have seen.&lt;br&gt;
You have a loan application workflow. 50,000 active applications. You need to add a compliance check step — but only for applications that went through the paid verification path, because that is the regulatory requirement for that specific path.&lt;br&gt;
Your database has 50,000 actors. Some paid the verification fee. Some waived it. Both groups are currently in &lt;code&gt;awaiting_documents&lt;/code&gt;. They are in the same state. They look identical to any query that reads current state.&lt;br&gt;
Any system that routes migrations by current state will treat them identically. That is the wrong answer.&lt;br&gt;
The correct answer requires looking at each actor's history. An actor that processed &lt;code&gt;PAY_FEE&lt;/code&gt; belongs in the new compliance check flow. An actor that processed &lt;code&gt;WAIVE_FEE&lt;/code&gt; does not. The only way to know which group an actor belongs to is to look at what events it has already processed.&lt;br&gt;
This is the problem we built StateKeep to solve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How StateKeep handles it&lt;/strong&gt;&lt;br&gt;
StateKeep is a self-hosted statechart hosting platform. You deploy XState-compatible JSON definitions via HTTP, spawn actors, and send events. Any backend language works — Python, Go, Java, Node, anything with an HTTP client.&lt;br&gt;
When you deploy a new version, you declare a &lt;code&gt;historyPath&lt;/code&gt;:&lt;br&gt;
&lt;u&gt;json&lt;/u&gt;&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;"loan-v2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"loan-v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"historyPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SUBMIT_INFO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAY_FEE"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"definition"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="err"&gt;...&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;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;StateKeep evaluates every actor. Each one carries a fingerprint of its event history — a rolling FNV-1a hash of every event type it has processed in order. Actors whose history matches the declared path migrate. Actors whose history does not match stay on the current version.&lt;br&gt;
Alice paid the fee. Her fingerprint matches. She migrates to &lt;code&gt;loan-v2&lt;/code&gt;, where the new definition routes her through the income verification step before approval.&lt;br&gt;
Bob waived the fee. His fingerprint does not match. He stays on &lt;code&gt;loan-v1&lt;/code&gt;, continuing normally.&lt;br&gt;
Both actors keep running. Neither restarts. Neither loses context. No migration script. No midnight deployment anxiety.&lt;br&gt;
The routing is not based on engineering confidence. It is backed by a formal mathematical proof that guarantees every actor ends up on exactly the correct version, with no actor evaluated twice, regardless of timing or evaluation order. The algorithm runs in native C at p50 1.26µs per actor — 50,000 actors in under a second on modest hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it looks like in practice&lt;/strong&gt;&lt;br&gt;
&lt;u&gt;typescript&lt;/u&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@statekeep/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-instance.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk_...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Deploy a machine definition&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submitted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;submitted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;SUBMIT_INFO&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;under_review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;under_review&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;PAY_FEE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;awaiting_docs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="na"&gt;WAIVE_FEE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;awaiting_docs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;awaiting_docs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;APPROVE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;REJECT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;final&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;rejected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;final&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Spawn one actor per loan application&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;applicantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;usr-001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;loanAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25000&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Send events as things happen in your system&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT_INFO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PAY_FEE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Later — deploy a new version targeting only paid-path actors&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newDefinition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;historyPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT_INFO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PAY_FEE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// Only actors who processed PAY_FEE migrate. Everyone else stays.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can preview the migration before committing:&lt;/p&gt;

&lt;p&gt;&lt;u&gt;typescript&lt;/u&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;preview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newDef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;historyPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT_INFO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PAY_FEE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;migration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wouldMigrate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1,203&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;migration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wouldStay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// 847&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The preview uses the exact same evaluation function as the live deployment. What you see is what will happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does not do&lt;/strong&gt;&lt;br&gt;
StateKeep tracks state. It does not execute your code.&lt;br&gt;
Guards (&lt;code&gt;guard: 'isEligible'&lt;/code&gt;) are ignored entirely — every transition fires unconditionally when the matching event arrives. Do not rely on guards to protect invalid transitions. Check eligibility in your backend before calling send. Actions (&lt;code&gt;actions: 'sendEmail'&lt;/code&gt;) are no-ops — state changes but nothing executes. Your backend reads the new &lt;code&gt;stateValue&lt;/code&gt; from the response and handles side effects itself.&lt;br&gt;
This is a deliberate design choice. The engine is pure data: JSON definitions in, state transitions out. No secrets, no database connections, no application context. Your business logic stays in your application where it belongs.&lt;br&gt;
The upside: migrations never accidentally re-fire side effects. 50,000 actors migrating to a new version do not trigger 50,000 emails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The current state&lt;/strong&gt;&lt;br&gt;
StateKeep is at early access. The platform is running in production, 400+ tests passing, the APV engine active, self-hosted on a VPS with AES-256-GCM encryption at rest, continuous backup via Litestream, full dashboard, CLI tooling, and a TypeScript SDK.&lt;br&gt;
We are looking for developers who have hit the XState persistence problem or the workflow migration problem in production — people who have written that midnight migration script, who have lost actor state on a server restart, who have kept two versions of workflow code running forever because there was no clean upgrade path.&lt;br&gt;
Free access for anyone willing to give honest feedback. Reach out at &lt;u&gt;&lt;a href="mailto:statekeep.support@gmail.com"&gt;statekeep.support@gmail.com&lt;/a&gt;&lt;/u&gt; or comment below.&lt;/p&gt;

</description>
      <category>xstate</category>
      <category>node</category>
      <category>backend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>We built a statechart hosting platform where two actors in the same state can migrate to different versions — here's why that matters</title>
      <dc:creator>StateKeep</dc:creator>
      <pubDate>Thu, 21 May 2026 15:10:04 +0000</pubDate>
      <link>https://dev.to/statekeep/we-built-a-statechart-hosting-platform-where-two-actors-in-the-same-state-can-migrate-to-different-18n1</link>
      <guid>https://dev.to/statekeep/we-built-a-statechart-hosting-platform-where-two-actors-in-the-same-state-can-migrate-to-different-18n1</guid>
      <description>&lt;p&gt;If you have built anything with long-running stateful workflows — loan approvals, order processing, subscription lifecycles, insurance claims, onboarding funnels — you have probably hit a wall that nobody talks about cleanly.&lt;br&gt;
You need to change the workflow. But you already have thousands of instances running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem nobody has a clean answer to&lt;/strong&gt;&lt;br&gt;
The standard options are all painful.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Wait for instances to drain naturally. Fine if your workflows complete in minutes. Useless if they run for weeks or months waiting for human approval, document submission, or payment settlement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Write a migration script. You query your database, move rows between tables, pray nothing is mid-transition, and hope you did not accidentally re-trigger a side effect for 40,000 customers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep old code running forever alongside new code. Now you are maintaining two versions of your business logic indefinitely, and the operational complexity compounds with every release.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Temporal's approach: version markers in your workflow code. This works, but it means every code change requires careful getVersion() calls throughout your workflow function, and a non-determinism error on a long-running production workflow is a genuine incident. We have seen threads from teams where a change they believed was backwards-compatible broke in rare production scenarios after deployment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;None of these answers are wrong exactly. They are just the best available options in a space where the fundamental problem — migrating running stateful instances to a new version of their logic — has never been solved cleanly.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What we built&lt;/strong&gt;&lt;br&gt;
StateKeep is a statechart hosting platform. You upload an XState-compatible machine definition, spawn actors against it, and send events. StateKeep handles persistence, event history, encryption at rest, and version migration.&lt;br&gt;
The part that is different: when you deploy a new version, each running actor migrates based on its event history fingerprint — not its current state.&lt;br&gt;
Every actor carries a compact hash of every event type it has processed, in order. When you deploy a new version with a historyPath, the platform checks each actor's fingerprint against the path you declared. Actors whose history contains that path migrate. Actors whose history does not contain it stay on the current version.&lt;br&gt;
The consequence: two actors in the same state can receive different migration decisions in the same deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The concrete example&lt;/strong&gt;&lt;br&gt;
A loan application workflow. Two customers, Alice and Bob. Both are currently in awaiting_documents.&lt;br&gt;
Alice paid the verification fee to get there. Bob waived it.&lt;br&gt;
You deploy a new version that adds an income verification step — but only for customers who paid the fee, because that is the regulatory requirement for that path.&lt;br&gt;
You declare:&lt;br&gt;
&lt;u&gt;json&lt;/u&gt;&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;"loan-v2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"loan-v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"historyPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"START_APPLICATION"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SUBMIT_INFO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAY_FEE"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"definition"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="err"&gt;...&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;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;The platform evaluates every actor. Alice's history contains that path. She migrates to loan-v2, landing in the new income_verify state. Bob's history does not contain PAY_FEE. He stays on loan-v1, continuing to awaiting_documents as before.&lt;br&gt;
Both actors keep working. Neither restarts. Neither loses context. No migration script was written. No side effects were re-fired. The decision was made per-actor, based on history, in under a second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this looks like in practice&lt;/strong&gt;&lt;br&gt;
Deploy a new version targeting a specific path:&lt;br&gt;
&lt;u&gt;typescript&lt;/u&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@statekeep/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-instance.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk_...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Deploy v2 — only actors who paid the fee are eligible&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loanV2Definition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;historyPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;START_APPLICATION&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT_INFO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PAY_FEE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Deploy a wildcard version — all actors migrate&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orderV2Definition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// no historyPath = all actors eligible&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Preview what will happen before committing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;preview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loanV2Definition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;historyPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;START_APPLICATION&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT_INFO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PAY_FEE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;migration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wouldMigrate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1,203 actors&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;migration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wouldStay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// 847 actors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The preview calls the exact same evaluation function as the live deployment. What you see is what will happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What StateKeep does and does not do&lt;/strong&gt;&lt;br&gt;
StateKeep is a state tracker, not a side effect executor. It does not run your action handlers or evaluate your guards.&lt;br&gt;
Guards (guard: 'isEligible') are stubbed to false — guarded transitions never fire. Actions (actions: 'sendEmail') are no-ops — state changes but nothing executes. Your backend reads the new stateValue from the event response and handles side effects in its own code.&lt;br&gt;
This is intentional. It means migration never accidentally re-fires side effects. An actor migrating from v1 to v2 does not trigger emails, charges, or notifications — because StateKeep never ran any of those in the first place.&lt;br&gt;
The supported pattern: model routing decisions as explicit events rather than guards. Your backend evaluates the condition and sends APPROVE_FAST_TRACK or APPROVE_STANDARD. The machine routes deterministically from there. No guards needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rescue deployments&lt;/strong&gt;&lt;br&gt;
When a buggy version reaches actors before you catch it, you deploy a rescue version targeting only the actors whose history includes the buggy path:&lt;/p&gt;

&lt;p&gt;&lt;u&gt;typescript&lt;/u&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan-v2-rescue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fixedDefinition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loan-v2-buggy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;historyPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;START_APPLICATION&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT_INFO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PAY_FEE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TRIGGER_BUG&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only actors whose history contains TRIGGER_BUG migrate to the fix. Everyone else is unaffected. No system-wide freeze. Forward-only. No rollback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The audit trail&lt;/strong&gt;&lt;br&gt;
Every routing decision is logged. For every actor evaluated in a deployment, there is a record of: which version it was on, which version it moved to (or why it stayed), its history fingerprint at decision time, and the registered prefix hash it was compared against.&lt;br&gt;
GET /v1/actors/:id/decisions returns the full routing history for a single actor. When a customer asks "why didn't my application get the new income verification step," the answer is in the database, not in a support ticket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Early access&lt;/strong&gt;&lt;br&gt;
We are at early access stage. The platform is running on a VPS, 432 tests passing, real migration engine deployed.&lt;br&gt;
We are specifically looking for developers who have hit the workflow migration problem in production — people who have written migration scripts they were not happy with, people who have hit non-determinism errors on Temporal after a versioning change, people who have kept old workflow code running forever because they had no other option.&lt;br&gt;
Free access, no strings attached. We want honest feedback from people who understand the problem space. If that is you, reach out at &lt;u&gt;&lt;a href="mailto:statekeep.support@gmail.com"&gt;statekeep.support@gmail.com&lt;/a&gt;&lt;/u&gt; with a sentence about what you are building. We will get you set up.&lt;br&gt;
We are not looking for validation. We are looking for the edge cases we have not thought of yet.&lt;/p&gt;

</description>
      <category>workflow</category>
      <category>statemachine</category>
      <category>backend</category>
      <category>node</category>
    </item>
  </channel>
</rss>
