Every few weeks, someone emails me asking: "Is there a desktop app? I don't want to work on architecture diagrams in a browser tab."
Fair enough. When you're deep in a system design session, the last thing you want is to accidentally close the tab, or have your diagram competing for attention with 47 other tabs. You want a dedicated window. A real app.
A progressive web app (PWA) is an ordinary web app that ships a manifest file and a service worker, which lets the browser install it as a standalone window with its own icon and no address bar.
That's the route I took for Codelit, and I'd take it again over Electron.
Why not just ship an Electron app?
Electron would have worked, but it costs a 200MB+ download, a second build pipeline, code signing on two platforms, and an update server to keep alive. For one person maintaining everything, that's four new jobs in exchange for removing a URL bar.
I seriously considered it. Electron is the default choice for "web app that wants to be a desktop app." VS Code, Slack, Discord, Figma (sort of), all Electron.
But the tradeoffs are rough for a solo dev:
- 200MB+ download size. Electron bundles Chromium. Every Electron app is a full browser. For a tool that's already a web app, this feels absurd.
- Separate build pipeline. You need electron-builder or electron-forge. Separate CI/CD. Separate release process. Code signing for macOS and Windows.
- Auto-updates. Electron has auto-update, but you need to host the update server. Squirrel, electron-updater, or roll your own. All of them have quirks.
- Two codebases diverge. The web version and the Electron version start as the same code, then slowly drift apart. A bug fix here doesn't make it there. Features ship at different times.
That last one killed it. Codelit has been a single Next.js codebase since the Lucidchart rage-quit that started it, and I wasn't about to grow a second one.
For a team of 10? Electron is fine. For a solo dev maintaining everything? It's a tax I can't afford.
What does it take to make a web app installable?
A web app becomes installable with three things: a manifest.json describing the app, a service worker, and HTTPS. Codelit already had HTTPS, so the entire desktop app was two new files and zero changes to the application code.
The manifest tells the browser "this is an installable app." It defines the app name, icon, theme color, and display mode.
The key property is "display": "standalone". This makes the installed app open in its own window without the URL bar, tabs, or browser menu. It looks and feels like a native app.
Losing the URL bar also loses the fastest keyboard target on the screen. In a tab, people navigate by typing in the address bar; in standalone mode there is nothing to type into, which is why the ⌘K command palette with 28+ shortcuts stopped being a power-user nicety and became the way you move around.
Which caching strategy does each asset type need?
Fonts and images use CacheFirst, API calls use NetworkFirst, and page shells use StaleWhileRevalidate. The split matters because Codelit's AI generation has to reach the network, while the canvas, templates, and UI should load on a dead connection.
For the service worker itself, I'm using next-pwa. It integrates with Next.js's build process and generates the service worker automatically.
The same config, read as a decision table:
| Asset type | Strategy | What happens with no connection | Why that one |
|---|---|---|---|
| Fonts (Google Fonts) | CacheFirst | Renders normally | They never change; a network round trip buys nothing |
| Images, icons, SVGs | CacheFirst | Renders normally | Same reason; capped at 100 entries for 30 days |
| /api/* responses | NetworkFirst | Last response, up to 5 min old | Freshness beats speed here; cache is the fallback |
| Page shells | StaleWhileRevalidate | Previous version, then updates | Instant paint now, correct on the next load |
Architecture generation is the one thing that can't degrade gracefully. It routes across 11 providers, and every one of them is on the far side of the network.
How do you show your own install prompt?
Intercept the browser's beforeinstallprompt event, call preventDefault() on it, stash the event, and fire prompt() from a button of your own. Browsers do surface a native install affordance, but it lives in the address bar and almost nobody looks there.
The event fires when the browser decides the app meets its installability criteria: manifest, service worker, HTTPS, and some engagement heuristics it doesn't document precisely. I intercept it, suppress the default prompt, and show my own UI instead: a subtle banner at the top of the page that says "Install Codelit as a desktop app" with an install button.
The display-mode: standalone media query at the top is the bit people forget. Without it, you cheerfully offer to install the app to someone who is already running the installed app.
Why won't the install prompt appear on iPhone?
Safari on iOS does not implement beforeinstallprompt, so there is no event to intercept and no button you can wire up. The only install path on iPhone and iPad is the Share sheet, and the most you can do in code is detect the situation and give instructions.
Android is the easy case. Chrome shows the install button in the address bar, and my custom prompt works there too.
iOS is... different.
It's not elegant, but it's the ceiling iOS gives you.
| Platform | How install actually happens | What you can trigger from code |
|---|---|---|
| Chrome / Edge on desktop | Install icon in the address bar, or your button | The full beforeinstallprompt flow |
| Chrome on Android | Native prompt, plus your own banner | The full beforeinstallprompt flow |
| Safari on iOS | Share button, then Add to Home Screen | Nothing. Detect and instruct only |
The offline banner
The offline banner is one fixed bar that appears when navigator.onLine flips to false and tells you editing still works but AI features need a connection. Nothing gets disabled and nothing gets a fake spinner.
If you lose connection mid-session, I want you to keep working, just without the AI features.
Simple, honest, non-blocking. The banner tells you what works and what doesn't. No fake loading spinners.
The React Flow performance problem I didn't see coming
Panning and zooming were smooth in a browser tab and janky in the standalone PWA window on some devices, because a standalone window doesn't always get the same GPU acceleration a tab does. Two CSS declarations closed the gap.
Here's a fun one. The interactive architecture canvas I built with React Flow worked perfectly in the browser. Then I installed the app, opened a large diagram, and panning was choppy. Same code, same machine, same browser engine.
will-change: transform tells the browser to promote the element to its own compositing layer. contain: layout style tells it that layout changes inside the node don't affect anything outside. Together, these hints give the browser permission to optimize aggressively.
After adding these, performance in standalone mode matched the browser. Large diagrams pan and zoom smoothly on both.
What changed after people installed it?
Installed Codelit users average 22-minute sessions against 11 minutes for browser users, roughly double, and come back 3.5x more often. Six months in, 12% of active users have installed it, and the maintenance overhead compared to the plain web version is close to zero.
The session length difference makes sense. A dedicated window means the user is intentionally doing architecture work, not casually browsing. And there's no "accidentally close the tab" problem, because closing the app window is a deliberate action.
The return rate is the bigger win. An app icon on the dock or home screen is a constant reminder that the tool exists. Browser bookmarks don't have the same effect.
When is Electron still the right call?
Electron is the right call when you need native file system access, system tray integration, or genuinely deep OS hooks. Codelit is a web canvas with AI features on top, and it needs none of those, so the comparison fell one way on every line that mattered to me.
| Dimension | PWA | Electron |
|---|---|---|
| Install size | ~0 MB (uses existing browser) | 200+ MB |
| Auto-updates | Automatic (just deploy) | Needs update server |
| Build pipeline | Same as web | Separate |
| Codebase | Identical to web | Starts identical, diverges |
| Cross-platform | Works everywhere | Needs per-platform builds |
| Maintenance | One pipeline, no more | Significant |
The auto-update row is the one that decided it. When I deploy a new version of Codelit, every user, web and installed, gets it immediately. No "please update your app" prompts, no version fragmentation, no supporting an old release because a chunk of users never clicked the updater.
Which is really a delivery argument dressed up as a platform choice. The number of pipelines you maintain sets a hard ceiling on how often you can ship.
Install Codelit yourself
Installing Codelit takes one click. Visit codelit.io in Chrome, Edge, or any Chromium-based browser, and you'll see an install icon in the address bar. Click it. Now you've got a dedicated architecture design app on your machine.
On mobile, Android users get the install prompt automatically. iOS users: tap Share, then Add to Home Screen. (I know. Blame Apple.)
The app is the website. The website is the app. And I maintain one codebase.
If your product is already a web app and it doesn't touch the file system, the desktop version you keep meaning to build probably already exists. You just haven't written the manifest yet.
Questions people actually ask
- What is a progressive web app?
- A progressive web app is an ordinary web app that ships a manifest file and a service worker, which together let the browser install it as a standalone window with its own icon and no address bar. There is no separate binary and no second codebase. The installed app is the same site running in its own window, and it picks up every change the moment you deploy it.
- Should I build a PWA or an Electron app?
- Build a PWA if your product is already a web app and does not need native file system access, a system tray icon, or deep operating system hooks. Electron gives you all three, but it costs a download of over 200 megabytes, a separate build pipeline, code signing on both macOS and Windows, and an update server you have to host and keep running yourself.
- How do I make a Next.js app installable?
- Add a manifest file describing the app name, the icons, and a display mode of standalone, register a service worker, and serve the site over HTTPS. For Next.js, the next-pwa package hooks into the build and generates the service worker for you, including the runtime caching rules. No changes to the application code itself are required to make the install button appear.
- Why can I not install a web app on my iPhone?
- Safari on iOS does not implement the beforeinstallprompt event, so a website cannot show you an install button the way Chrome can on desktop and Android. The install path exists, but it is manual. Open the page in Safari, tap the Share button, and choose Add to Home Screen. Developers can detect iOS and display those instructions, and that is as far as it goes.
- Does an installed PWA work offline?
- Partly, and it depends entirely on the caching rules in your service worker. In Codelit, fonts, images, and the interface are served from cache and load with no connection, so you can keep editing a diagram offline. Anything that calls the API, including AI architecture generation, needs the network. A banner appears when the connection drops and says exactly which features stopped working.