Daniel's been building a home inventory system for years now — started after a move where he couldn't find half his stuff, and it's grown into this full-featured warehouse-grade tool running on a VPS with a Vue frontend, Postgres, TypeScript backend, Drizzle ORM, media on Cloudflare R2 with pre-signed URLs. He directs an AI to write most of the code. The phone is the natural interaction surface — QR scanning, photos, quick lookups — so he's been using it as a PWA through Hermit on Android. His question is threefold. One: what is he actually sacrificing by not building a native Android app? Two: is there a clean path to keep one codebase that works for both the web deployment and a native APK? And three — this is the ambitious one — is there a deployment method where a single push updates both the production container on the VPS and the phone app, with no manual intervention?
And this lands at a really specific moment. The gap between PWAs and native has been shrinking for years, but the last twenty percent — background processing, camera pipelines, reliable push — that's still a real gap in twenty twenty-six. Daniel's use case makes it unusually visible because he's hitting almost all of those edges at once. Batch AI jobs that run overnight, QR scanning that needs to be fast and reliable, offline resilience for when he's in a part of the apartment with no signal, notifications when the batch finds something. A PWA can do versions of all of these. The question is whether the versions are good enough.
So this is the story of one developer's bespoke tool, but the questions it surfaces are universal for anyone maintaining a private, production-grade side project. The gap between "it works" and "it works optimally" when you're the sole developer, sole user, and sole QA team.
And Daniel's stack is well-chosen. Vue with TypeScript, Postgres with Drizzle — that's a modern, type-safe setup. Containerized on a VPS, media offloaded to R2 so the container stays lean. The PWA approach through Hermit gives him a home-screen icon, offline caching via service worker, camera access through the Web Camera API. For maybe eighty percent of what he does day to day, this setup is completely fine. The question is what lies beyond that threshold, and whether the cost of crossing it is worth paying.
Let's start with the concrete gaps. What does a PWA actually lose compared to a native Android app, feature by feature?
Background sync is the big one, and for Daniel's architecture it's the most consequential. He's got this AI batch job that runs overnight — it takes photos he's snapped during the day, extracts serial numbers, part numbers, manufacturer details, and populates fields. That job runs on the server. The phone doesn't trigger it; it's scheduled server-side. And that's not a design choice — that's a constraint. PWAs on Android, even wrapped in Hermit, cannot do reliable background sync without the Web Periodic Background Sync API. That API exists, but browser support is inconsistent. Chrome on Android has it behind flags. Hermit's WebView doesn't expose it at all. So the phone can't say "hey server, run the batch now and tell me when it's done." The server has to be the one that decides when to run, and the phone finds out later.
Which means if Daniel snaps a photo of a new vacuum cleaner at eleven PM and wants the AI to process it overnight, he's not triggering that from the phone. He's relying on a cron job on the VPS that runs at, say, two AM regardless. If the photo didn't upload properly — maybe he was in a dead zone — the batch runs without it and he doesn't know until morning.
Native Android solves this with WorkManager. You define a task — "upload pending photos, then call the batch endpoint, then notify me" — and the OS schedules it. It handles retry logic, it respects battery constraints, it survives reboots. And it works even if the app is force-stopped. That last part matters more than people realize. If Daniel swipes the PWA away to free memory, the service worker might or might not still be alive depending on how Android's memory management is feeling that day. WorkManager doesn't care. The task is registered with the system, not tied to the app process.
So background sync is the headline loss. What about the camera? Daniel's QR scanning workflow — he's scanning codes on boxes, on items, on shelves. That's a frequent operation.
The Web Camera API in a PWA gives you one-shot capture. You tap the button, the camera opens, you take a photo, the photo is sent somewhere for decoding. If you want real-time barcode scanning — the kind where you point the camera at a QR code and it instantly recognizes it with a little overlay on the preview — that requires a video stream. PWAs can access the camera stream via getUserMedia, but the performance path for running a barcode detection library on each frame in a WebView is... not great. You're decoding in JavaScript, in a browser context, with no access to the hardware-accelerated image processing pipeline. It works. It's just slower, drains more battery, and misses codes more often than native.
And native uses what?
ML Kit on Android. Google's machine learning SDK. It has a barcode scanning API that runs directly on the device, no network round-trip, hardware-accelerated. You point the camera, it scans thirty frames a second, it finds the code in under fifty milliseconds. The difference in feel is enormous. With a PWA, you hold the phone steady and wait. With native, you wave the phone vaguely in the direction of the code and it's already scanned. For a tool Daniel uses every time he adds an item to inventory, that friction adds up.
The thirty frames a second versus "hold still and pray" gap. Got it. What about file storage? He's caching product photos for offline use.
Right. So the PWA can use the Cache API through the service worker to store assets for offline access. But we're talking about photos here — multi-megabyte images. Browsers impose storage quotas on PWAs. Chrome on Android typically gives you somewhere between twenty and sixty percent of the device's free space, but it's opaque, it can change without warning, and when the browser decides it needs to reclaim storage, your cached photos are the first thing to go. There's no "pin this file, never evict it" API. Native Android, you write to the file system. You decide where the photos live. You control retention. For offline resilience — Daniel's in a basement storage room with no signal, trying to pull up a photo of what's inside box seventeen — native means the photo is definitely there. PWA means it's probably there, unless Android decided to clean house.
Push notifications. Daniel mentioned the AI batch finding something — a duplicate serial number, a missing field — and wanting to alert him. How does that differ?
PWA push works through the browser's push service. The server sends a push event to the browser's push endpoint, the service worker receives it, and shows a notification. But — and this is the part that trips people up — the browser has to be running. Not necessarily in the foreground, but the process has to be alive. If Daniel force-stops Chrome or Hermit, push stops working until he opens the app again. Native Android push via Firebase Cloud Messaging works even if the app is force-stopped. The notification is delivered at the OS level, and Android wakes up the app to handle it. For a tool that might need to say "hey, the batch found a serial number conflict, come look at this," that reliability gap is real.
And performance? He's got a growing inventory — could be thousands of items. Scrolling through a categorized list of five thousand assets in a PWA.
Vue is lightweight. The virtual DOM is efficient. But at the end of the day, you're rendering to a WebView. Every frame of that scroll is going through the browser's layout engine, compositor, and GPU pipeline. A list of five thousand items with images, labels, categories — that will jank. You'll see dropped frames, stutter when you fling-scroll. Native Android uses RecyclerView with a view holder pattern. It only renders the items that are visible on screen plus a small buffer. The scrolling is handled by the OS compositor, not a JavaScript framework. It's buttery smooth at any list size. For an app Daniel uses daily, that tactile difference matters more than you'd think. You don't notice smooth scrolling, but you absolutely notice when it's not smooth.
And Hermit — Daniel mentioned he prefers it over the native PWA install flow. What's Hermit actually doing?
Hermit wraps the PWA in a standalone WebView with some extra permissions and UI customization. It gives you a proper home-screen icon, it isolates cookies and storage from your main browser, it lets you customize the toolbar and notification behavior. It's a better PWA container than Chrome's "add to home screen." But it's still a WebView. It doesn't add any native APIs that the WebView doesn't already support. It doesn't give you background execution, it doesn't give you ML Kit, it doesn't give you WorkManager. It's a convenience layer, not a bridge. Daniel's getting a nicer PWA experience, but he's not getting any closer to native capabilities.
So those are the sacrifices. Background processing, camera speed, storage reliability, push guarantees, scroll performance. The PWA is fine for the eighty percent case. The last twenty percent is where native pulls ahead. But here's the thing — you don't have to accept all of those. There's a path to keep one codebase and still get native access.
This is where Capacitor comes in. Capacitor is from the Ionic team, and it's the most mature option for wrapping a web app into a native Android APK while keeping a single codebase. The way it works: your Vue app stays exactly the same. The same components, the same routing, the same state management. Capacitor wraps it in a native Android project — a Gradle-based build — that contains a WebView shell. But unlike Hermit, Capacitor's WebView is instrumented with a plugin bridge. That bridge is the key. It lets your JavaScript code call native Android APIs through plugins that expose them as JavaScript functions.
So instead of using the Web Camera API and hoping for the best, you use the Capacitor Camera plugin, which opens the native camera with preview, with ML Kit barcode scanning, with full-resolution capture.
The plugin ecosystem covers almost everything Daniel needs. The Camera plugin gives you preview-based capture and real-time barcode scanning. The Filesystem plugin gives you persistent local storage with predictable paths — no browser quota eviction. The Background Task plugin wraps WorkManager, so you can schedule that overnight batch trigger reliably. The Push Notifications plugin hooks into Firebase Cloud Messaging, so notifications arrive even when the app is force-stopped. And because the UI is still your Vue app running in a WebView, you haven't forked the codebase. You write once, you get native capabilities on Android.
The single-codebase dream, with an asterisk. What's the asterisk?
The asterisk is that you now have two build artifacts. One codebase, yes. But from that codebase you produce a web app — the same one Daniel already deploys to his VPS — and a native APK. The web app is your PWA fallback, or your desktop interface. The APK is your Android app with full native access. They share the same Vue code, the same TypeScript logic, the same API calls to the backend. But they have different build pipelines, different deployment targets, and different update mechanisms. The codebase is unified. The artifacts are not.
And that's where Daniel's third question lands. The holy grail — one push that updates both the VPS container and the phone app, no manual steps.
Let's walk through what that pipeline actually looks like. Daniel pushes to main on his repository. A GitHub Actions workflow triggers. Step one: build the Docker image, push it to a registry, SSH into the VPS, pull the new image, restart the container. That's the server side — it's well-understood, it's what he's probably already doing or close to doing. Step two: the same workflow builds the Capacitor Android project. That means running the Gradle build, which produces a signed APK. The signing key lives in GitHub Secrets — you generate it once, store it securely, and the CI pipeline uses it to sign every build.
Then distribution. The APK has to get to the phone somehow.
A few options. You can host the APK on the VPS itself — a simple endpoint that serves the latest build, and the phone app checks it on launch. You can use Firebase App Distribution, which is Google's tool for distributing pre-release builds to testers. Or — and this is the one I think fits Daniel's use case best — you can use Capgo. Capgo is a service built specifically for Capacitor apps. It provides over-the-air updates for the web layer of your app without rebuilding the native APK. Here's how it works: the native shell — the Capacitor WebView plus plugins — is installed once via APK. After that, when you push new code, Capgo delivers the updated JavaScript bundle directly to the app. The app downloads it in the background, and on next launch, it's running the new code. No APK reinstall, no manual steps, no app store.
So the native shell is the stable layer — it changes rarely, when Capacitor itself updates or when you add a new plugin. The Vue code changes constantly, and those changes arrive OTA through Capgo.
That's the model. And it gets Daniel remarkably close to the one-push dream. The GitHub Actions workflow would look like this: push to main, build Docker image, deploy to VPS, build Capacitor web bundle, upload to Capgo, and optionally send a push notification to the phone saying "update available." The phone app checks with Capgo on launch, sees there's a new bundle, downloads it silently, and applies it on next restart. The entire pipeline fires from one git push. The server updates automatically. The phone updates automatically — as long as the native shell hasn't changed.
And when the native shell does change — a Capacitor major version bump, a new plugin that needs native permissions — then you do need to build and install a new APK. That's the asterisk on the asterisk.
Right. APK installation still requires user action. Android's security model won't let an app update itself silently — that's a feature, not a bug. But in practice, Capacitor core updates are infrequent. The plugin set for a tool like Daniel's stabilizes quickly. You might need to side-load a new APK once or twice a year. Everything else — the Vue components, the API logic, the UI tweaks — arrives OTA through Capgo. It's ninety percent of the way to "one push, zero manual steps." The remaining ten percent is Android's security boundary, and that's not something you want to work around even if you could.
There's another option emerging — Tauri Mobile. Rust-based, positioning itself as a lighter-weight alternative to Capacitor. Where does that stand?
Tauri Mobile is interesting. It uses the platform's native webview — so on Android it's the system WebView, not a bundled Chromium — and the backend logic runs in Rust instead of JavaScript. The Rust component compiles to native code, which means it can be significantly faster for compute-heavy operations and uses less memory. The APK sizes are smaller too — Tauri apps can be a fraction of the size of an equivalent Capacitor app because they don't bundle a JavaScript runtime. But as of September twenty twenty-six, the Android support is still maturing. The plugin ecosystem is thinner — you'd be writing more native bridges yourself. For a Vue/TypeScript stack, Capacitor is the pragmatic choice today. Tauri is one to watch, especially if Daniel's AI-generated codebase ever shifts toward Rust on the backend.
Let's talk about the update experience side-by-side. PWA, Capacitor with Capgo, and native APK. What does Daniel actually see when he pushes code?
With the PWA, the update arrives on next page load. The browser fetches the new service worker, the service worker caches the new assets, and on the next navigation or refresh, the user sees the new version. It's automatic but opaque — you don't know you've been updated unless you check the version somewhere. With Capacitor and Capgo, the app checks for updates on launch. If there's a new bundle, it downloads it in the background. The user keeps using the current version. Next time they open the app — or if you configure it to prompt — the new version loads. You can show a small banner: "update ready, tap to restart." It's slightly more visible, slightly more controlled. With a pure native APK approach — no Capgo — every update means downloading and installing a new APK. Android shows the install dialog. The user has to tap "install." It's friction every time. That's why Capgo is the secret sauce here. It collapses the update experience for the web layer back to something that feels automatic.
The architecture Daniel's looking at is: one Vue codebase, Capacitor for the native shell, Capgo for OTA web updates, GitHub Actions to tie it all together, and a private APK hosted somewhere for the initial install and the rare native shell update. That's a real answer to his question. It's not a single artifact, but it's a single codebase with a single push that updates everything that changes frequently.
The trade-off is complexity in the build pipeline versus simplicity in the user experience. Setting up Capacitor means adding an Android project to the repo, configuring Gradle, managing signing keys, setting up the Capgo integration. It's not trivial. But once it's set up, it runs. Daniel's AI agent can maintain it the same way it maintains the rest of the codebase. The CI pipeline is a YAML file and some secrets. The ongoing cost is low.
There's a misconception worth naming here. People talk about PWAs and native converging, and in some dimensions they are. WebGPU is coming. WebAssembly is getting faster. But for background processing, push reliability, and camera-based workflows — the exact things Daniel's app depends on — the gap remains significant in twenty twenty-six. It's not closing quickly. It's being bridged by tools like Capacitor rather than by the platform itself.
Another misconception: single codebase means single build artifact. It doesn't. Capacitor keeps the codebase unified but the build process produces two things — a web app and an APK. They have different deployment pipelines, different update cadences. The unification is in the source, not in the output. That's not a dealbreaker; it's just a fact of the architecture that's worth being clear about from the start.
We've got the gaps mapped, the bridge identified, the deployment pipeline sketched out. But before we declare Capacitor the winner, Hilbert's been sitting over there for the last ten minutes.
Hilbert: They're fine.
...go on.
Hilbert: The QR scanner. The background jobs. The offline photos. All of it works, you said so yourself. He's been using the PWA for years and the system has survived two moves. Two moves. That's the metric. Not frame rates. Not whether the notification arrives when the app is force-stopped. The system did its job. Twice. And now you want him to add a Gradle build and an APK signing pipeline and a third-party service for over-the-air updates so he can scan a QR code half a second faster. I built an inventory system in two thousand three for a friend who ran a rare book dealership. FileMaker Pro, a PowerBook G4, and a barcode scanner that plugged into the USB port. The scanner cost four hundred dollars. It beeped when it read a code. That was the notification. The database file is still on a USB drive in my desk. It tracked twelve thousand books across three warehouses. Never dropped a frame because there were no frames. You're solving a problem he doesn't have yet.
Daniel's inventory is growing. The AI batch is new. The QR scanning friction — he might not have complained about it, but he asked the question. He's feeling the ceiling.
Hilbert: He asked the question because he's an engineer and engineers can't leave well enough alone. I respect that. I am one. But the question wasn't "should I rebuild my entire deployment pipeline." The question was "what am I sacrificing." And the honest answer is: not much, for what he actually does day to day. The batch runs on the server. The QR codes — how often is he scanning in a basement with no signal? If the answer is almost never, the PWA camera is fine. The offline photos — when was the last time he needed to see a photo of a vacuum cleaner and had no connectivity? He lives in an apartment in Jerusalem, not a submarine.
You're not wrong about the usage patterns. But you're missing something. Daniel enjoys the engineering. The system isn't just a tool — it's a project. The question about native versus PWA isn't only about what he needs today. It's about what's interesting to build.
Hilbert: I built the FileMaker system in three weekends. It ran for eight years. The friend sold the business. The database is still on the USB drive. I haven't opened it since two thousand eleven. The point of a tool is to disappear. The best inventory system is the one you don't think about. If he's spending his Saturdays tuning a CI pipeline to shave milliseconds off a barcode scan, the tool isn't disappearing. It's becoming the hobby.
That's a different question, though. Whether the tool should be a hobby versus whether it can be improved. Daniel's clearly in the camp of "both." The system works, and he wants to make it work better. The fact that it's survived two moves doesn't mean it's done evolving.
Hilbert: Fair. But I'd still tell him to count how many times last month he was actually offline when he needed the app. Count how many times the QR scan was too slow. Count how many times he missed a notification because the app was force-stopped. If the answer to all three is zero — and I suspect it is — then Capacitor is a solution to a problem he doesn't have. Yet.
Yet is the operative word. His inventory is growing. The AI batch is adding complexity. The QR scanning is becoming more central to the workflow. The ceiling he's feeling is real, even if it hasn't caused a failure yet.
Hilbert: I had a ceiling once. The FileMaker system hit it when the book dealer wanted to add online sales. FileMaker didn't do web. So we exported to Excel, manually. Every week. For two years. Was it elegant? No. Did it work? Yes. The business ran. Sometimes the ceiling is just the ceiling and you live with it.
Hilbert's not wrong, but he's also not entirely right. The question Daniel's really asking might not be about features at all. It's about whether the joy of building the thing optimally is worth the complexity of the build pipeline. And for someone who's already directing an AI to write most of the code, adding Capacitor and Capgo to the stack is incremental complexity, not a rewrite.
The open question I keep coming back to is this: for a private tool with a single user, does the marginal gain from native features justify the complexity of a second build pipeline? The answer depends entirely on how much you value the engineering versus how much you value the result. Daniel clearly values both. For someone who just wants the tool to work and never think about it again, the PWA is fine. Hilbert's FileMaker story proves that — a tool built in three weekends ran a business for eight years. But Daniel's not that person. He's the person who asks "what am I sacrificing" because he wants to know, even if the answer doesn't change what he builds tomorrow.
The landscape is shifting. WebGPU and WebAssembly are blurring the line between web and native. In a few years, the PWA gap might be small enough that this whole conversation is moot. But right now, Capacitor offers a pragmatic middle path — one codebase, native access when you need it, OTA updates when you don't. It's not the holy grail of a single artifact, but it's close enough that the remaining gap is measured in APK installs per year, not per deploy.
If this episode made you think differently about your own private tools — or if you've built something weird that only you use and you've been wrestling with the same trade-offs — send it in. We love the weird stuff. Email us at show at my weird prompts dot com.
This has been My Weird Prompts. Thanks to our producer Hilbert Flumingtop.
We'll be back soon.