
4 min. read
WinUI 3 migration: Unlocking system-wide capabilities on Windows
Passbolt moves from UWP to WinUI 3, to improve Windows password management with better system integration, security features, and long-term platform support.

Passbolt's Safari extension is finally live. What should have been a straightforward browser extension took four years due to multiple platform changes, a couple of hibernations, one miraculous build-mode fix, and more asterisks in documentation than any developer should ever have to deal with. This is the story of how it got built, reconstructed from notes, screenshots, and increasingly unreliable memories.


It started on a Friday afternoon, by the coffee machine. Because apparently that's where all major decisions are made. The whole project was born in just two sentences:
"Why don't we support Safari, by the way?"
"Well, we tried, but I think we couldn't make it stable."
That was enough. I'd also heard about a tool that converts an existing Chrome extension into a Safari extension automatically. It sounded like something that works out of the box, needing at most a few small adaptations.
I downloaded the converter, installed Xcode, read the EULA, sold my soul, and ran it. It spat out something with a generous collection of warning messages. Safari took issue with quite a few things:
Fine, let's do it without those for now. I then needed to build the final package via Xcode, which refused and gave about a hundred reasons why. I started removing features, adapted the easier ones, and eventually convinced it.
For the first time, I had a neat little application showing a single button asking me to click it to open Safari. Safari opened the extension settings page and... nothing. The extension wasn't there. After configuring Safari to allow it, something appeared. I could activate it, and the Passbolt icon showed up in the toolbar. Plugged in and running!
Well, It just made an acte de présence. It showed up for attendance, then mentally checked out. Clicking the icon opened an empty bubble, and the pages that should have loaded were blank.
Then, by accident, I noticed faint black letters on a dark grey background. Barely visible. The extension was running. Just without any style.
I patched that. After reducing cookie security, bypassing a few features, patching some, and forcing others, I had the extension working. "Working" here meant refreshing the page about ten times before anything useful happened. A niche yet memorable experience. The quick access worked more or less. The main app was still blank. And Passbolt is a password manager; without passwords to manage, it's really just an ambitious browser ornament..
Still, poking around, I found that by combining resource access with some manipulation in the quick access, I could get something showing on the main screen. I ran into my favorite rubber duck again:
"How did you get it to start? Sometimes it loads, sometimes it doesn't."
"It never really worked. We never managed to make it work."
"I have something though. I can see the resource workspace, but it can't load data unless I start with the quick access first."
The duck and I spent some time pair programming. Mostly the duck. When the extension showed up again, they said:
"We never got this far."
The following Monday, I was asked to write a document listing everything that still needed fixing. I genuinely thought it would take around six months.
It took four years.

Nothing was truly working or stable enough to distribute. A lot had to be removed, including some fairly critical features. "Barely usable" is the honest description, and completely unusable if someone else had the extension running on another browser at the same time. The result was technically a password manager, in much the same way a shopping cart with three wheels is technically a vehicle.
The list of problems was long: theme management kept breaking usability, the cookie issue blocked sign-in entirely, some data validation had been stripped out, the resource workspace showed up but rarely loaded any passwords, and the application would sometimes just fail to start, unpredictably.
It felt manageable at the time. But this was just the surface. Some issues were hiding deeper ones that only became visible once the first layer was fixed. Some required significant rewrites.
And then, in parallel, came the Manifest Version 3 project.
Google had decided that browser extensions needed to migrate to a new standard, with a hard deadline. Whether MV3 was an improvement remains a matter of lively debate, but the migration wasn't optional. Miss it, and your extension stops being distributed. It swallowed a year of engineering time and it became the highest priority we had. Safari remained on the sidelines, drinking from a tiny paper cup and waiting to be called back into the game.
The irony is that the instability we'd been living with would likely have been resolved by that migration anyway. Safari was going to benefit from work we weren't even doing for Safari.
Months became years, through a series of reasonable decisions.
A manifest is a file bundled with an extension that declares what features it needs: downloading files, writing to the clipboard, managing tabs, and so on. The migration became one of those projects that slowly expands until it's the only thing anyone talks about. Meanwhile, we still had features to ship and bugs to fix.
Meanwhile, on my own time, I was back on Safari.
I had to be. This project had become an obsession. I stopped counting the hours, the sleepless nights, the attempts that should have worked and didn't, each one stopped by a single small detail. By then, I was no longer experiencing frustration. I had become its vessel.
The new codebase had made a difference, and Safari's updates helped too. Some bugs had fixed themselves, without AI (there was none really good at the time). Others, naturally, remained, presumably protected by long-term support agreements.
MV3 is the new standard and we'd need to move to it eventually, so why not make Safari work with it straight away? The main challenge turned out not to be about features at all. It was the developer experience.
Safari's feedback when an MV3 extension fails to load? "It didn't start because of an issue." That's it. No details, no stack trace, nothing. Some forums suggested running the extension in Chrome first to see what error appeared. Sometimes that worked. You'd fix it, Chrome would stop complaining. Safari would look at the corrected code, shrug, and say: "It doesn't work."The problem was now Safari-only. The tooling had a strong "trust your instincts" philosophy.

I also went the hard route: downloading WebKit, the engine behind Safari, and compiling it manually. Five coffees later, I realized I made it, I was now being paid to supervise a compiler. Eventually I had a freshly built WebKit. Very satisfying, honestly. Except Safari restricts extension support in that mode. All that compilation time, gone.
The developer experience was genuinely bad. It felt very on-brand for a company founded by a man who thought suffering built character. One of the major changes in MV3 is that the extension can shut down without notice. Safari embraced this concept enthusiastically. In Safari specifically, it stops or starts without any signal. If something goes wrong, it stays alive but can't restart. Just blocked. That's not an experience we can ask end users to live with, and there was no clean workaround.
The debugging tools weren't fully working either. The console was present, some messages appeared, but the network panel was completely empty with no way to see what was being sent or received. But at least it made it easy to read.
I eventually rolled back to MV2, where I could see everything and restart the extension on demand. When you're developing, you need fast iteration. Waiting 30 seconds after every change is painful enough. Being completely blind on top of that is not workable.
We recently gave MV3 another attempt before releasing Safari. The tooling works even less now; the console doesn't display a single word. For now, we're staying on MV2. Apple hasn't set a deadline for it anyway.
One of the harder challenges with Safari is its update pace. Competitors are at version 100-something. We're still seeing Safari 16 and 17 in the wild, because Safari updates ship alongside full OS updates. This means Safari is necessarily behind on available features, and required significant adaptation to bring the extension in line with the others, or at least as close as possible.
Safari is genuinely trying to align with W3C standards. It just gets there on Apple time.. Meanwhile, bugs needed fixing with no visibility into their roadmap. Wait for Safari to catch up, or handle things ourselves?
We handled things ourselves.
The biggest discovery came from a small detail in the MDN documentation I had missed, about local storage, the feature used to store Passbolt data inside the extension. Like other browsers, Safari supports everything related to that feature, including triggering code execution when stored information changes. That's what the documentation says.
But there was a little asterisk I had missed.
Works on all browsers. Except when using iframes on Safari.
We use iframes.
The entire Passbolt interface runs inside one. For a password manager, having the interface isolated from the page we're interacting with is generally considered a good idea. We thought we were following security best practices. Safari treated it more like a creative suggestion.
That single difference was enough to prevent the entire application from working. The fix required fully rebuilding our local storage layer so that changes could be manually signaled to anything listening. Our architecture didn't support that by default, so new building blocks had to be created just to make it possible, before even starting the actual work.
Two years had passed at that point.
Two new problems immediately followed: First, this change touched every other browser extension too, carrying a real risk of breaking things for existing users while bringing nothing new to them. Second, now that the resource workspace was actually working, all the bugs that had been hiding behind it were finally visible.
Any modification made to a resource wasn't being reflected on the page. Users would have needed to hit CMD+R every time to see their changes. This is not generally considered a modern synchronization strategy.
New excavation site unlocked. Software engineering occasionally resembles archaeology, except the ruins are yours.
Other parts of the extension had their own issues. File downloads simply don't work out of the box on Safari. Chrome uses one approach, Firefox uses another, Safari is incompatible with both. The workaround we landed on was to route downloads through the native macOS application that Safari extensions run alongside, which writes the file directly to the filesystem and opens the destination folder. Not elegant, but functional within our constraints. That communication channel would prove useful again later.
Then there was still the cookie problem. Safari checks cookies outside our sandbox and strips the ones it considers should not be sent, including our session cookie. Every request went out unauthenticated. Safari was protecting us from ourselves. The eventual solution: delegate every request to Passbolt servers through that same macOS application running in the background. Step entirely outside Safari's sandbox. It can't filter cookies it can't see. By this point, the native application had become our smuggler.
Priorities being what they are, Safari had to wait again. We're not a large team, and keeping one person on seemingly unsolvable problems indefinitely isn't sustainable. Other projects moved up the queue.
At some point, I brought it up with the rubber duck by the coffee machine:
"We should take an hour and talk about what to do with the Safari project. I think we should give up on it."
The response was pragmatic: let's check if anyone actually uses Safari. We have community signals; let's see if it's worth continuing. The duck remained focused on the breadcrumbs.
We sat down and looked at the numbers together. The community signals were clear: it was worth continuing. The duck was annoyingly data-driven. I pushed back anyway. The work ahead was still enormous, and Safari would slow down every future release, not just this one.
I had walked into that meeting ready to pull the plug. I walked out with a roadmap.
In hindsight, that hibernation period is what saved the project. Apple had been working on their side: new updates, new features, a significant number of fixes. Things that would unblock us later.
When the project restarted, things had improved. SSO sign-in, which I hadn't touched, just worked. The in-form menu worked. The quick access worked.
The cookies did not work.

Going back to the cookie API, I noticed it had been updated. Cookies were now fully accessible, including those marked httpOnly, but only when requested from a specific "cookie store." Most developers never have to think about this. A glimmer of light.
It did not work. Safari checks cookies outside our sandbox anyway and removes the ones it considers shouldn't be sent. Back to square one.
Talking it through with Cédric over coffee (at an actual coffee shop this time, not the office machine), I remembered an old idea I'd abandoned because I couldn't access all the cookies back then. Now I could. So I tried it. And it worked.
Progress continued. At some point, only one problem remained: the resource workspace data refresh.
Other prominent ducks from our pond, Benj and Sowmya, who were working in parallel on the React 17 to React 18 migration, noticed it might fix the Safari issue. React 18 included specific improvements to state management on Safari. The catch: that migration is a significant undertaking. It took about half a year.
Before it finished, Anto, our resident kingfisher from the support team, told me he was eager to test even with the last bug still present. Support engineers are a special breed. While developers avoid bugs, they actively swim towards them. For once,I wanted to give him something clean. This was an unusual experience for both of us. I rebuilt the extension in release mode, did a quick check before handing it over.
Everything worked. Even the resource workspace. I stared at the screen for a while.
I had done nothing different except switching the build mode, which shouldn't affect behaviour. Somehow it did. After years of debugging, the final bug had apparently been solved by asking the compiler to be serious this time.
With something working, the next step was internal testing via TestFlight. The upload process is manual: package a zip, upload to App Store Connect, respond to clarifications, wait for the build to become available.
It didn't work.

The binary from TestFlight was different from the one I had uploaded. Comparing them carefully, it turned out App Store Connect had replaced part of it. This is generally not what you want from a file hosting service. Turns out when you upload a zip manually, App Store Connect wraps the extension with a default native application, effectively overwriting the communication mechanisms we had built. Uploading directly from Xcode with a local certificate stops it touching the binary. That worked.
Internal testing began. The UAT phase ran fairly smoothly. Small bugs were found and fixed. One issue stood out: on certain processes that made a lot of requests in a short window, the extension crashed. The cause was the communication channel between the extension and the macOS application hitting its concurrency limit. Safari limits concurrent calls but returns control to the extension before fully closing each one, so the count climbs faster than it falls. In essence, Safari was telling us the calls were finished before they had finished finishing.
The fix was to wait for each call to fully release before starting the next. Some processes became slightly slower as a result. A small price to pay at this stage.
One month after the first internal TestFlight release, the same package, untouched, stopped working.
This is where the React 18 migration came in. It fixed it. The resource workspace, the data refresh, all of it. Thank goodness.
Apple won't publish an application without a review, which is reasonable. It ensures a quality bar and protects the end-user experience. What's less talked about is the pace: one message a week in our case, and a refusal at any hint of uncertainty on their side. The review process felt less like a conversation and more like a correspondence. Every interaction felt like a turn-based strategy game.
Passbolt isn't accessible with just a username and password, which made setting up the reviewers to actually test the application a process in itself. Beyond that, explaining how to test an unfamiliar product clearly enough that nothing gets misinterpreted is harder than it sounds. We're close to our own product, and what's obvious to us isn't obvious to others. Even a simple copy-paste instruction can go multiple ways. This is how entire QA careers are born.
There were questions about our business model, bugs found during review, permissions the converter had added that we didn't need, misunderstandings about certain flows. Some of the business model questions felt like they were coming from the company that owns the marketplace and wanted to make sure everybody's interests were properly aligned. All normal questions. But with one message a week with a faint "nice business you have there" energy and no visibility into when the next review cycle would start, it stretched out.
We had planned to release alongside the other extensions. We missed that by a month. Since we release once a month, it felt like losing an entire release.
Every day I checked whether they were reviewing, whether they had responded. No ETA, no timeline. Occasionally, an email: refused.
And then, one Friday afternoon (a Friday afternoon, again), a different email arrived.
I opened App Store Connect and checked the application.
Validated.
That button was finally there. The one that would push this extension live on the App Store. After four years.
I know my fellow developers will judge me for this: I clicked it on a Friday!

Here it is: Passbolt's Safari Extension

4 min. read
Passbolt moves from UWP to WinUI 3, to improve Windows password management with better system integration, security features, and long-term platform support.

5 min. read
Learn how to safely purge large action_logs tables in Passbolt without causing downtime, using a controlled, step-by-step strategy, and understand when and how to reclaim disk space effectively.