DEV Community

Cover image for I shipped an app to the Microsoft Store. It has no code.
Eugeniya Ivanova
Eugeniya Ivanova

Posted on

I shipped an app to the Microsoft Store. It has no code.

We got Publora into the Microsoft Store. I expected this to involve building a Windows app. Instead, we ended up submitting something that contains almost nothing except pictures and instructions to open our website.

I should mention that I don't write the code myself. Claude does. I usually handle the questions, the testing and the occasional argument about whether something is actually finished. This time there wasn't much code for either of us to worry about.

I unpacked the .msix package we were submitting and looked through its contents. Eighty-four files. Eighty of them PNGs, accounting for almost all of the package size. The remaining four were the manifest and some XML files. No .exe, no .dll, no .js.

The manifest tells Windows to launch our PWA through Edge:

--app-id=fjgokhecjlkghhifolnkkipaippljcnm
--app-fallback-url=https://app.publora.com/
--display-mode=standalone --windows-store-app
Enter fullscreen mode Exit fullscreen mode

PWABuilder generated the package. What users install is essentially a standalone window for the Publora website, without the browser's usual address bar. We didn't need to write a separate Windows application.

A package with no code asked for full trust

When we uploaded the packages, both triggered a warning about runFullTrust. That's a Windows capability normally associated with desktop applications that need broader system access.

I was looking at a package containing eighty PNGs and wondering what exactly they were planning to do with all that trust.

Partner Center wanted an explanation. I used the wording from the PWABuilder guide: "Needed for PWA Hosted App model, created by pwabuilder.com." The submission passed certification.

I wouldn't treat that as a guarantee for every submission, though. The guidance we found about this warning was from 2020 and 2021, and Microsoft may handle it differently now.

The agent did everything except the last button

Claude handled most of the Partner Center process through a debug Chrome session and Playwright. Registration, identifiers, nine separate fields for the D-U-N-S number, package uploads, screenshots. There was plenty of clicking involved, and Partner Center's custom web components weren't always cooperative.

The submission itself was where Claude stopped.

The first attempt failed with a network error. On the second, Claude's safety classifier blocked the action with the message "judged this action dangerous." No further explanation.

It didn't try to get around the restriction. Instead, it got everything ready and left the final submission to me. I clicked Submit for certification, and later Publish now.

There were also the Microsoft Developer Agreement and the IARC age declaration, including the confirmation that the person accepting it is legally an adult. Those are things I needed to agree to myself.

So after Claude had worked through all the forms, I got the important job of confirming that I was old enough to use them.

The wait wasn't where they warned

Microsoft says company verification typically takes five business days. Ours took about fifteen minutes. Certification can take up to three business days; ours finished in just under two.

What took longer than expected was testing.

We needed to open the package on a real Windows machine before submitting it. Unfortunately, everyone on our team uses a Mac. We spent most of a day finding someone with Windows and then waiting for them to wake up in their time zone and try it.

The Microsoft checks were quick. Finding an awake person with Windows took longer.

curl said everything was fine

We found one problem only because we opened the app in a clean browser.

When someone visits the root of our site while signed out, the browser sends them to the landing page rather than the sign-in screen. But curl gets a 200 response from the same URL, with no HTTP redirect.

The redirect happens in the browser, so the command-line check didn't show it. We added a note for the reviewer explaining where to go if they encountered the same thing.

It's the sort of detail that's easy to miss when your tests all say the URL works.

Updates go around the store

The package is version 1.0.1, but the application itself loads from our website. When we deploy changes to Publora, those changes appear in the installed app without submitting another package to Microsoft.

That's particularly convenient for us because we're updating Publora all the time. I wouldn't want to go through store certification every time we changed something in the interface.

Our service worker already has a comment explaining why the app shell is always loaded from the network:

// the app shell (index.html / navigations) is ALWAYS served from the
// network, never from cache. A cached shell would reference old
// content-hashed chunks that a fresh deploy has already deleted (404),
// which is exactly what freezes navigation after a tab is left open
// across a deploy.
Enter fullscreen mode Exit fullscreen mode

That behaviour wasn't implemented specifically for Microsoft Store. It's part of how our web app handles deployments, but it happens to be useful here too.

Why a wrapper over the website at all

Publora is now in the Microsoft Store, listed under Productivity. It's free, we didn't have to change the website, and the first submission was approved.

You could reasonably ask why we bothered when the result is essentially our website in a separate window.

Mostly because we're trying different ways for people to discover Publora. We've submitted integrations and listings to quite a few directories by now. Some bring users, some barely move the numbers, and it's difficult to predict which will be worth the effort.

This one was relatively easy to do. PWABuilder generated the package, we already had the PWA, and there was no separate Windows codebase to maintain.

I don't know yet whether the Microsoft Store will bring us any users. The listing has only been live for a day. We'll see.


I'm also curious how other people handle the final steps when working with agents. Do you let an agent submit and publish things on your behalf, or do you prefer to press those buttons yourself?

Top comments (9)

Collapse
 
alexcodebytes profile image
Oleksandr •

The runFullTrust thing is genuinely absurd — 80 PNGs asking for system-level trust is the kind of detail that should make reviewers ask harder questions, not pass on a quoted line from a 2021 guide.

On your closing question: I press the buttons myself, and the classifier blocking it is the reason I'd keep doing that. The frustrating part isn't the refusal — it's "judged this action dangerous" with no explanation. A refusal you can't reason about is a refusal you can't safely work around, so the agent stopping and handing back control is exactly right. The failure mode I'd worry about is an agent that doesn't stop.

The curl 200 vs browser redirect catch is also a good reminder that "the URL works" and "the URL works for a human" are different tests.

Collapse
 
eugeniya_ivanova_4a58eadc profile image
Eugeniya Ivanova •

Yeah, “judged this action dangerous” with nothing after it is the part that stuck with me too. A refusal you can’t reason about is one you can’t safely override, so I’d rather it lean toward stopping than toward confidence.

“An agent that doesn’t stop” is the right thing to worry about. What made this one fine wasn’t that the classifier was correct, I still have no idea if it was. It’s that the agent treated its own uncertainty as a reason to hand back, not an obstacle to route around. I’ll take a cautious handoff over a confident wrong move any day.

And yes, “the URL works” and “the URL works for a human” turned out to be two different tests, which is more or less the whole reason that section exists.

Collapse
 
alexcodebytes profile image
Oleksandr •

"Treating its own uncertainty as a reason to hand back, not an obstacle to route around" is the exact principle more agents need baked in. Most RLHF training actively punishes stopping or refusing, so getting an agent to say "I can't do this safely" is actually a huge win for alignment. A cautious handoff is always better than a confident hallucination.

Thread Thread
 
eugeniya_ivanova_4a58eadc profile image
Eugeniya Ivanova •

Yeah, and the incentive you’re describing isn’t only in the training. It’s in how we use these things day to day. I catch myself rewarding the agent that just gets it done and quietly resenting the one that stops to ask. If “I can’t do this safely” only survives in the lab and gets trained out by impatient users like me, we lose the exact behavior we say we want.

The part that stays with me is that the one refusal I couldn’t reason about did more for my trust than a hundred smooth completions would have. I’d rather it stop for a bad reason than sail past a good one.

Thread Thread
 
alexcodebytes profile image
Oleksandr •

The "impatient user" problem is the whole loop in miniature: caution costs you time now, and the mistake it prevents costs you time later — so the impatient brain always votes against it. That's exactly why it has to survive in the product, not just the lab.

On the unexplainable refusal: I'd add one caveat. It earns trust only if the reason turns out to be real. A refusal that later proves to have been pointless costs more trust than a smooth completion ever earns — so the agent owes you the explanation eventually, even if not at the moment it stops.

Thread Thread
 
eugeniya_ivanova_4a58eadc profile image
Eugeniya Ivanova •

Fair correction, “stop for a bad reason” was me being glib. You’re right that a refusal which turns out pointless and never explains itself costs more than a smooth wrong answer, since it burns a round-trip and some trust on the way.

The narrower thing I’d defend: a stop with no reason in the moment is fine only if I can reconstruct the reason later. The refusal that never becomes explainable is the one that does the real damage. Mine sat right in hindsight because I could go read what it balked at and see why a classifier might flinch. If I’d gone looking and found nothing, it would have crossed from cautious into just unreliable.

Thread Thread
 
alexcodebytes profile image
Oleksandr •

That's a really sharp distinction. "Unexplained in the moment" is a UX choice, but "permanently unexplainable" is a system failure. The real engineering challenge is making sure your telemetry captures the exact trigger, so that looking back later actually yields an answer instead of a shrug.

Thread Thread
 
eugeniya_ivanova_4a58eadc profile image
Eugeniya Ivanova •

Right, and the uncomfortable part is that the refusal is the event that most needs a trace and usually gets the least. We instrument the happy path to death and treat a stop as a dead end, when it’s the one moment where “why” is the whole value.

In my case the telemetry was basically luck. I could reconstruct it by hand because the thing it balked at was sitting right there in the session. At any real scale that doesn’t hold, and “it stopped” without the trigger logged is just a shrug with a timestamp.

Thread Thread
 
alexcodebytes profile image
Oleksandr •

"Just a shrug with a timestamp" is the most accurate description of 90% of debugging sessions I've ever had. We spend so much time logging every successful query and then just throw a generic Error 500 into the void when it actually breaks. The "why" is exactly where the value is, but it's always the last thing we build.