October 26, 2026
Building Internal Platforms around Developers’ Needs
Author: Catalin Ciubotaru, Staff DevEx Engineer
Discover how Miro built an internal feature flagging platform around developers’ needs, from faster SDK initialization to clearer rollout workflows. This engineering overview explores the decisions, migration, and lessons behind making experimentation and releases easier.
What good iteration should enable
Your team has an idea for improving the product. You build it, ship it, and now you need to know whether it actually helped. Or maybe you’ve replaced something deep in the application and would like to know that it works before everybody gets it. That sounds reasonable. 🛟
An experiment lets us compare a change against the existing experience and test a hypothesis. A gradual rollout lets us introduce it to, say, 10% of users, watch performance and errors, then increase exposure as we gain confidence. And if something goes wrong, a kill switch lets us turn the feature off without waiting for another deployment. I covered these building blocks in Experimentation, this is the way, if you’d like a little more background.
At Miro, these controls help engineering, product management, and design learn from changes and release them with more confidence. My work in DevEx is to make that experience clear, effective, and enjoyable, from configuring a rollout to consuming a flag in application code.
This is how we approached Miro’s Iteration Platform: help people make changes with confidence, understand and test behaviour, and spend less effort getting there (and maybe sprinkle a little whimsy along the way 🤭). I’ll walk through the problems, the engineering decisions, and what we learned from the people using it. This is the overview; there’s more coming on SDK design, migration, debugging, and the other interesting things hiding underneath. 🔧
Where the existing experience fell short
Before I joined Miro, teams used a third party feature flag solution to configure experiments and rollouts. It gave us a working foundation. People still had to translate what they wanted to achieve into its targeting rules, though, and that translation left quite a bit of room for interpretation 😅.
Take “release to Mironeers.” A Mironeer is someone who works at Miro. Simple enough, right? One person might target everyone with a Miro email address. Another might target everyone belonging to the Miro organization. A third might actually rely on a list of userId of Mironeers. All people would describe their release the same way, while actually targeting different groups.
Every team had to work out that definition themselves. Ideally, they could choose a shared audience and trust that it meant the same thing across releases, without needing to know how we calculated it underneath.
We also had a less negotiable problem: timing. We wanted to compare a new rendering engine with the existing one and understand its effect on board loading and the user experience. But the app had to choose its rendering engine very early during startup. In our setup, that decision came before the Vendor SDK was ready, and we couldn’t afford to wait for it. That kind of experiment was simply not possible.
There was another reason some flags had to wait. To decide what an organization should receive, we needed to know which organization it was and, for some rules, which plan it used. The client only gathered that information later in startup. Making the SDK faster wouldn’t help those flags unless we also made their targeting information available sooner.
What we chose to improve and why
So, what could we do about it? I helped evaluate the options: continue with the current vendor, ask our provider for help, consider another SaaS solution, or build more of the platform ourselves. There were two problems to solve: help people configure releases consistently, and make flag values available when the application needed them.
Keeping the third party solution was a reasonable option. It worked, its costs were predictable, and it required little internal platform investment. We could also build our own interface around it, translating Miro’s release paths into its configuration. That would let people follow a shared sequence of audiences and rollout stages instead of reconstructing the rules every time.
That tailored layer was useful enough that we actually built it. But however friendly we made the UI, the browser would still need to wait for the underlying SDK. To support decisions early in startup, such as selecting a rendering engine, we needed to change how evaluation worked.
In our existing setup, the SDK downloaded flag definitions and evaluated them locally. The browser had to download and store those rules before it was ready, including targeting information we didn’t want exposed there. We’d also had an incident where local storage filled up and functionality stopped working. There was more to fix here than a loading spinner. 😅
We chose remote, eager evaluation: calculate variants on the server in advance and send the client the results it needs. The browser would receive a smaller payload of evaluated values, without needing the targeting rules or evaluation engine locally. Combined with resolving the required context earlier, this gave us a way to support those early decisions.

Two servers, different jobs: the server that serves Miro app, initialises the user session and resolves the context; the SDK passes it to flag-evaluator and caches the evaluated values it gets back.
That was the case for owning evaluation as well as the interface. It also meant taking on the service’s reliability, SDK maintenance, updates, and support ourselves. With two of us on the platform team, we had to be deliberate about what we built and how we introduced it. That meant designing how the pieces would fit together, then introducing them in stages.
Building and introducing the foundation
We needed three pieces to work together: the management interface where people configured flags, the service that evaluated them, and SDKs that made the results available to applications. I owned the product surface and designed and built the client-side SDK; my colleague owned the backend and infrastructure. We also built on research and reviews from other engineers.
We used OpenFeature for standard interfaces and SDKs, with a provider connecting them to our evaluation service. I wrapped its web implementation to handle Miro’s authentication, context, and update needs. That gave us a common foundation while leaving room for the integration our application actually needed.
We built and released the management UI on top of the SaaS's existing APIs. Once it was available, people created and updated flags through our guided flows, and we translated their choices into the third party configuration underneath. They could start using the new release paths while flag evaluation continued through the established system.
Then we watched people use it and helped when they got stuck. Sometimes the guidance wasn’t clear; sometimes their release didn’t fit a path we’d considered. Working through those situations together showed us what to improve. Our customers were our colleagues, and we wanted them to feel comfortable telling us when something was confusing or simply annoying.
Once we were confident in that workflow, we introduced the new evaluator and SDKs underneath it. Remember the organization information we were waiting for during startup? The server serving the Miro app now resolved that context and sent it to the client. The SDK passed it to a separate service, flag-evaluator, which evaluated all flags and returned the results for the SDK to cache. By the time application code asked for a flag, its variant could already be there.
I kept the SDK’s API small: initialize it, read a variant, and update the evaluation context when the user’s situation changes. Switching organizations needs fresh results; changing a flag’s configuration needs an update too, which we pick up through polling. There’s enough in that lifecycle for its own post, so I’ll save the API and synchronization details for the SDK article.
There was another detail we couldn’t lose in the move: evaluating a flag doesn’t mean somebody has encountered the feature. We separated server-side evaluation from tracking when the application accessed a variant. Precomputing everything at startup shouldn’t make our experiment data say that everything was used.
We kept both evaluation systems running during the transition, with new flags going to the new stack. Existing flags could continue working while teams adopted the SDK. I also built familiar debugging tools, including URL overrides, so changing SDKs didn’t mean relearning how to test a feature.
We used AI to help with the remaining migration work. Across the migration, we moved roughly 1,000 flags with zero downtime and zero incidents, without stopping teams from shipping. Some people didn’t even notice the change underneath. I was very happy with that kind of boring. 🎉 The coexistence, validation, and rollback details deserve their own article.

From the original vendor stack to our own platform: replace the UI, introduce the new evaluator and SDK alongside with the third party one, then migrate the remaining flags.
Making it useful in everyday work
With the migration done, we could spend more time on the things that made our colleagues’ work difficult. We had the freedom to change the platform, and the people using it were close by. Their questions, ideas, and requests helped us decide where to go next.
Keeping track of all that became a project of its own. I wrote about the research hub I built to remember the work and decisions, and make them visible to colleagues. It helps me return to a topic without starting from scratch, and gives people a way to see what we’re thinking about and contribute. Here are two examples of the problems we worked on.
Understanding a result 🔎
A long-lived flag can accumulate quite a few targeting rules. Different groups enter at different stages, percentages change, and eventually someone wants to know what one particular organization will receive.
You could read the configuration and work through the rules yourself. But if people keep having to do that, we can probably give them a better way.
We built a feature called Simulation. Enter an organization ID and it shows which variant that organization would receive for a flag, and why. Global Simulation answers the wider question across flags: what experience is this organization getting?

What does organization 111 get? Simulation returns “off,” highlights the matched Experiment stage, and shows the reason alongside the configuration.
This is useful to anyone investigating that experience, whether they’re in engineering, product, or another team. We started with the difficulty people described and worked towards a way for them to answer it themselves.
Cleaning up afterwards 🧹
A feature reaches general availability and the team moves on to the next thing. The flag stays behind, using storage and CPU for evaluation and generating data that nobody is looking at anymore. Across many flags, that’s a lot of unnecessary work for the system. We wanted to remove that waste without making it our job to chase teams to clean up after every release.
We built automatic flag cleanup. Very creative name, I know. When a flag reaches an appropriate end-of-life state, such as a concluded experiment or general availability, we can trigger our robots 🤖 to prepare its removal.
They generate draft pull requests for the owners to review. The owners still decide whether the changes are correct and whether to merge them. What disappears is much of the manual preparation that makes cleanup easy to postpone.
This is the kind of work I like giving to my robots: repetitive preparation that leaves a concrete result for a person to review. It makes cleanup easier to fit into somebody’s day. The AI article will get into how we scope that work and where human review still matters.
What changed and what we learned
In our setup, SDK initialization went from roughly 2–7 seconds to 0.1–0.7 seconds. Flags were ready to be used in a fraction of the time. 🏁
The evaluation service is classified as P0 at Miro. In one seven-day EU-production snapshot, we recorded 17.6 million SDK initializations. A weekday afternoon snapshot gives another view of that scale: over 300,000 active user sessions in the EU alone, with the SDK determining which flag-controlled features each session gets. Those flags shape the experience people are having while they work, so getting the right variant to the right session, on time, matters.
That speed-up opened up experiments and rollouts involving decisions made early in application startup. Choosing a rendering engine is one example of that broader capability. Meanwhile, shared release paths reduced the targeting work people had to do, and Simulation made it easier to understand the result.
We also got things wrong. One example was an exclusion stage: a way to explicitly name people or teams who should not receive a feature.
We initially resisted building it. Our reasoning was that defining who gets a feature already defines who doesn’t: everybody outside that group. Why build this apparently unnecessary functionality?
Then consider a request to release to everybody except one team or two users. Expressing that through inclusion rules means describing everybody else. People knew exactly what they wanted to do, but our design made them spend extra time figuring out how to express it. We should have made that part easier. 🙃
Helping people work through those cases made the gap obvious. An explicit exclusion stage let them describe what they wanted directly, instead of working around our model.
That’s something I want to carry into the next thing we build. The SDK still needs to be fast and correct, the service needs to be reliable, and the API needs to make sense. We also need to spend time with the people using all of it, because they’ll show us the extra work we’ve accidentally left them.
There’s plenty more to unpack here. I’ll follow up with the SDK’s API and evaluation tradeoffs, migration and rollback, debugging and simulation, running the platform as a product, and the AI capabilities. For now, I hope this gives you a useful picture of how the pieces fit together, and a few things to look for in your own internal tools.
---
Take care of each other! 🧡