Oh, boy. It's been a while, and I'm sorry for that. I was busy learning... at least that's what I keep telling myself 😁. I've also been busy trying to understand why it takes a Mironeer (a person who works at Miro) around 27 clicks to configure a feature to be released to all Mironeers.
Did I get your attention 👀? Let's dive into what we built, and most importantly, why.
The Problem 🚩
At Miro, as any reputable tech company, feature flags are part of how the software is built, and when I joined, the picture was... quite scattered. We were using a SaaS solution, and the amount of confusion and issues it caused was quite unmanageable. Here are some examples:
- Want to run a flag very early on, before the app is even available, tough luck
- Want to debug why someone is getting a specific variant, well, you can't really
- Do your customers use ad-blockers? Well, your data might be lackluster then
- and incidents about wrong feature getting to wrong customers
- and incidents about broken app due to full local storage
- and more...
Just to make sure that sinks in, take the Miro release example. People were talking about doing a Miro release, but sometimes, they had a different definition of what that meant.
- Some people thought that it meant all the users with an @miro email address.
- Some people thought that it's all the users belonging to the Miro organisation.
- Some people even thought that it's all the users using the Miro VPN.
And the challenge was, all of these rules could be configured in the SaaS UI. Nothing telling you that you're wrong. Where there's no guardrails, no shared primitives, sky is the limit right?
The Solution 💡
In theory, the solution is simple: build it in-house. Wait, wait, don't jump at me. I know some of you prefer buy, and that's fine.
The actual solution here is converging the organization on the same primitives. What do I mean by primitives? I mean making sure that everybody is speaking the same language.
Take our Miro Release example. Mironeers should know what a Miro Release means, but they shouldn’t need to know how it translates into targeting rules. They should simply tell the platform: release this flag to all Mironeers. That's it. The platform should take care of the rest.
Just for fun, try to configure this setup using the two scenarios 👇
Ready to assemble the targeting rules.
So, what primitives did we learn about? 🚂
Before jumping on this train 🚂, we did several weeks of customer support to understand how people are using the current solution, what are they trying to achieve and where are they struggling. Based on all of this, using the primitive lens, here are some other primitives we extracted (we ended up calling them stages):
- Internal testing 🧑💻 (test your flag with your team for example)
- Experiments 🧪 (doing a 50/50 on your flag, for the purpose of analyzing the performance afterwards)
- Gradual Rollout 📈 (slowly, percentage wise, rollout to your audience, while watching the metrics for degradation)
- and more...
We found out that this is what people needed, not a bunch of mix and match targeting editors. The SaaS gave us all the building blocks 🧱, but every team still had to assemble Miro’s release practices themselves. Ok, this is promising. We have some solid ideas of improvements 🤩.
Well, the problem is more complicated than that
Now, this is only one lens on the problem. What other challenges did we have? We can group these challenges in two buckets:
- SaaS Solution challenges
- All the flags need to be downloaded locally => slow cold start 🐌
- Downloading flag definitions to the browser => any sensitive targeting data inside comes along for the ride 🔐
- Known tracking tool => blocked APIs 🚫
- Developer Experience challenges
- Flags not available early on => no way to run early experiments or rollouts 🙅
- Incidents caused for example when Local Storage is full 🔥
- Hard to debug the SDK since it's a bit of a Black Box ⬛
As you can see, we had plenty of challenges to pick from. Just to emphasize, the beauty of all of this is the fact that none of this is theoretical. This is stuff we learned by observing how the current platform is being used.
Now, to the fun part, the Solution Thinking 🧠.
What we want to have
Well, first, we want to have a better UX, that helps the user achieve what they want, and not cause more friction than it needs to. This needs to revolve around the aforementioned primitives. Then, we want to have an SDK that is faster and debuggable. We want to build the structure that can be used for all sorts of flags: temporary and long term, with and without metrics. We want to build a platform that the EPD (Engineering, Product, Design) want to use, and keep coming back to.
All of this to say: we wanted to build what became the Iteration Platform 🚀, a platform that makes people's jobs easier and more enjoyable ❤️.
How we built it
Building a system that ticks those boxes is not a trivial matter. Especially when you start thinking about migration from the current system to this new system. Also, to avoid headaches around authorization, hosting etc, we built this in Backstage. This, being behind Okta, allows us to automatically tick that box.
I led this initiative across product direction, UX, frontend and SDKs, working alongside teammates who owned the backend and infrastructure.
Now, we split this project into three pillars:
- The infrastructure
- The SDKs
- The UI/UX
We'll dive into each in separate articles 📝.
How we released it
We first built a new UI on top of the APIs offered by the SaaS solution. This allows us to build something to fix the primitives problem. Under the hood, this UI would still convert the primitives to rules in the SaaS product. This would have a few advantages:
- We will still rely on the current solution
- Same SDKs would work
- In case of emergency, the old UI can still work
- This will allow us to validate and iterate on the primitives we discovered
We kept both UIs available while we validated and iterated on our stages. Once we felt relatively confident, we removed access to the old UI and “forced” everyone onto the new flow. This introduced some friction on purpose:
- If the stages covered the use case, people could continue on their own 👍
- If they didn't, we helped them through the old UI—but only after understanding the missing use case and deciding whether the Iteration Platform should support it 🧑🎓
This was a huge learning opportunity. It allowed us to understand a lot about what was being released, how, and what was the plan for the feature.
Next steps:
- Add the backend that uses the new stack for storing the targeting rules, in parallel with the old system.
- Then, turn off the old system, so all new flags are created in the new system.
- After a while, migrate what's left.
Here's a more... dynamic illustration of the migration process 👇
What do we have now
Let's go back to our Miro Release example. Well, now it takes people 3 clicks to configure something to be released to all Mironeers. No way of getting it wrong, everyone speaks the same language 🤝.
But wait, there's more! Our SDK went from booting up in 2s-7s to an average of 250ms 🏎️. We exposed the Iteration Platform through MCP, allowing developers to ask their AI assistants about running flags without switching context 🤖.
We gave the robots a broom 🧹. They prepare the flag-cleanup PR, but humans still decide whether it’s safe to merge or it needs further changes. We added simulation capabilities that offer a full picture of what a Miro Client experiences; what features they are getting and why. And more.
Trust reset
Trust wasn’t included in the first release 📦. We had to earn it through small steps, constant feedback, and showing people that the platform worked for them.
We spent a lot of time building human readable docs, that are whimsical, creative and easy to follow 📚. Once a question that was not covered by docs popped up, we would write the doc, and use that to answer the person. We hosted Office Hours, helping users debug, configure and release their features 🎙️. We gave workshops and hosted Iteration Days to keep things fun and educative 🎨.
Eventually, people started sending us love letters 💌. Well, slack DMs, but they told us the platform made their work easier and was genuinely enjoyable to use. That’s when we knew the trust was real.

How we are iterating
We're far from perfect. We're always learning about what people are struggling with, and we're always available to help 🫶.
Take, for example, the Exclusion stage. We were stubborn about adding it. On paper, exclusion was redundant: simply target the people who should receive the feature. Then users started bringing us increasingly complicated setups, such as releasing to all Mironeers except one team. The support requests made it clear that technically possible wasn’t the same as easy.
And that’s how you learn to listen 🧑🎓.
Scratching the surface
All this wall of text, is just scratching the surface of what we build, how we built it, and the lessons we learned. This is not the end, but the start of the story, and hopefully, you found it interesting enough to stay tuned 📻.
Stuff we learned
- Make the right thing easy, not the only thing possible.
- Listen before you talk, seriously.
- Build a safe environment where people don't feel like they're asking stupid questions.
- Be available and encourage people to experiment.
- You cannot over-communicate.
To be continued...
Be kind to each other! 🧡