WebVirt + Nexus: Run Your React/Vue/Svelte SPA Inside an Android WebView — No Capacitor, No Server, No Permissions
TL;DR — Two decoupled Android libraries: one serves your SPA from APK assets like a real web server, the other bridges JavaScript to native code. Used together in a production app serving local businesses. Open source, JitPack-ready. Engine v3 and Nexus v2 coming soon.
The Problem Nobody Talks About
You built a great SPA in React + Vite + TypeScript. It runs beautifully at localhost:5173. Now your client wants it packaged as an Android app.
Your options:
| Approach | Reality |
|---|---|
| Capacitor / Cordova | +25MB runtime, opinionated structure, black-box WebView |
file:// protocol |
CORS broken, SPA routes 404, APIs blocked |
| Embedded HTTP server (NanoHTTPD) | Threads, ports, network permissions, overkill |
| Raw WebView | You write everything from scratch |
What you actually want: your SPA thinks it's on https://app.local, everything comes from assets/, zero server, zero permissions, zero overhead.
That's exactly what WebVirt does.
What Is WebVirt?
WebVirt is a Java Android library that intercepts WebViewClient.shouldInterceptRequest() and serves your SPA assets directly from the APK — no actual server involved.
WebVirt.with(this)
.host("app.local")
.bind(webView);
webView.loadUrl("https://app.local/");
Your React app loads. Routes work. CORS works. No permissions needed.
Already published:
- GitHub: github.com/fouzstack/fouzstack-webvirt
- JitPack:
implementation 'com.github.fouzstack:fouzstack-webvirt:1.0.0'
Real Production Use Case
WebVirt v1 + Nexus are currently running in a financial management app used by small local businesses. The tech stack:
- ⚛️ React + Vite + TypeScript + Tailwind CSS v4
- 📦 ~5MB of assets served entirely from APK
- 📋 Product lists of 90+ items — fast scrolling, no lag
- 📤 JSON export (native)
- 📥 JSON import with file picker (no permissions)
- 📄 PDF export (native)
- 🔒 Restrictive CSP
- ⚡ Near-instant load time
No internet required. No server running. Just assets + native bridges.
What's Coming: WebVirt Engine v3 + Nexus v2
The production experience revealed what was missing. Two new libraries are in the works:
WebVirt Engine — The Production-Grade Upgrade
Built on the same shouldInterceptRequest() approach but adds what real apps need:
Smart caching with LRU + ETags
First load: index.html → read → ETag generated → cached (LRU)
Second load: index.html → ETag match → 304 Not Modified → 0 bytes transferred
Real Range Requests for video streaming
Client: GET /video.mp4 Range: bytes=1048576-2097151
Server: 206 Partial Content
Content-Range: bytes 1048576-2097151/52428800
[only the requested bytes — file never fully loaded into RAM]
Security headers on every response
Content-Security-Policy: default-src 'self'; script-src 'self'...
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Virtual API routes
WebVirt.with(this)
.host("app.local")
.route("/api/config", req -> json("{\"version\": \"1.0\"}"))
.cspPolicy("default-src 'self'; connect-src https://api.example.com")
.bind(webView);
The architecture uses Chain of Responsibility for request routing and Strategy for path handlers — the same GoF patterns you'd use in a backend framework, applied to Android's WebView layer.
Nexus — The Native Bridge
Nexus is a completely separate library for JavaScript-to-native communication. It doesn't know WebVirt exists. WebVirt doesn't know Nexus exists.
Nexus nexus = Nexus.installOn(webView)
.withDebugMode(false)
.withGlobalTimeout(30_000)
.registerHandler("export", new ExportHandlerAdapter(handler))
.registerHandler("import", new ImportHandlerAdapter(context))
.initialize()
.withFilePicker(activity);
nexus.attachToWebViewLifecycle();
From JavaScript:
const result = await window.__nexus.call("export", { format: "json" });
What makes Nexus interesting:
Smart re-injection — The JS runtime re-injects only on real page navigations, not on React Router hash changes (#/products, #/settings). This was a real pain point discovered in production.
// From the actual source
String baseUrl = url.contains("#")
? url.substring(0, url.indexOf("#"))
: url;
if (!baseUrl.equals(previousBaseUrl)) {
previousBaseUrl = baseUrl;
notifyPageLoaded(); // Only re-inject here
}
FilePicker without permissions — Uses Android's Storage Access Framework (SAF). No READ_EXTERNAL_STORAGE in your manifest.
nexus.withFilePicker(activity); // That's it. No permissions.
Interceptor chain — Add cross-cutting concerns (logging, auth, rate limiting) without touching your handlers:
Nexus.installOn(webView)
.registerInterceptor(new AuthInterceptor())
.registerInterceptor(new LoggingInterceptor())
.registerHandler("export", new ExportHandler())
.initialize();
Event emission from native to JS:
nexus.emitEvent("syncComplete", Map.of("records", 42));
window.__nexus.on("syncComplete", (data) => {
console.log(`Synced ${data.records} records`);
});
The Architecture: Two Libraries That Don't Know Each Other
This is the part I'm most proud of.
┌─────────────────────────────────────────────────────┐
│ WebView │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ WebVirt Engine │ │
│ │ shouldInterceptRequest() → assets/ │ │
│ │ "app.local/index.html" → real index.html │ │
│ │ CSP, ETags, Range Requests, LRU Cache │ │
│ └─────────────────────────────────────────────┘ │
│ ▲ │
│ │ Decorates (doesn't override) │
│ ┌─────────────────────────────────────────────┐ │
│ │ Nexus WebViewLifecycleObserver │ │
│ │ Observes onPageFinished, onPageStarted │ │
│ │ Delegates everything else to WebVirt │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Nexus JSInterface (parallel channel) │ │
│ │ window.__nexus.call("export", data) │ │
│ │ Does NOT go through shouldInterceptRequest │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
The key design decision: WebViewLifecycleObserver wraps WebVirt's client using the Decorator pattern. It adds lifecycle hooks without overriding or replacing WebVirt's request handling.
| WebVirt doesn't know... | Nexus doesn't know... |
|---|---|
| That Nexus exists | That WebVirt exists |
| There's a JS bridge | How assets are served |
| What handlers are registered | What virtual host is used |
This is actual decoupling, not just marketing copy.
Honest Comparison
| Feature | WebVirt Engine | Capacitor | Raw WebView |
|---|---|---|---|
| Runtime size | 0KB overhead | ~25MB | 0KB |
| Smart cache (LRU + ETags) | ✅ | ❌ | ❌ |
| Range Requests (video) | ✅ Real | Delegates to system | ❌ |
| Configurable CSP | ✅ | ❌ | ❌ |
| SPA route fallback | ✅ Automatic | ✅ | ❌ |
| Offline mode | ✅ Built-in | ✅ | ✅ |
| Hot reload (dev) | ❌ | ✅ | ❌ |
| Native JS bridge | With Nexus | ✅ | Manual |
| No permissions needed | ✅ | ✅ | ✅ |
| Interceptor chain | ✅ (Nexus) | ❌ | N/A |
| Setup time | ~5 min | ~2 hours | Variable |
Who Should Use This
✅ React/Vue/Svelte developers who need Android without Capacitor's complexity
✅ Small teams who want full control with minimal dependencies
✅ Offline-first apps (no internet required after install)
✅ Apps where clean architecture matters
❌ If you need live hot reload during development
❌ If your team is already deep in the Capacitor ecosystem
What's Available Now vs. Coming Soon
| Status | |
|---|---|
| WebVirt v1.0.0 | ✅ GitHub + JitPack |
| Nexus (production) | ✅ Running in production |
| WebVirt Engine v3.1.1 | 🔜 Coming soon — github.com/fouzstack/webvirt-engine |
| Nexus v2.0.0 | 🔜 Coming soon — github.com/fouzstack/nexus |
| Kotlin DSL (-ktx modules) | 🔜 Planned |
| Unit tests + benchmarks | 🔜 Planned |
When Engine and Nexus launch, I'll post a follow-up with concrete benchmarks: cold start times, cache hit rates, memory footprint.
Try WebVirt Now
// settings.gradle
dependencyResolutionManagement {
repositories {
maven { url 'https://jitpack.io' }
}
}
// build.gradle (app)
dependencies {
implementation 'com.github.fouzstack:fouzstack-webvirt:1.0.0'
}
WebVirt.with(this)
.host("app.local")
.bind(webView);
webView.loadUrl("https://app.local/");
Put your React/Vite build output in assets/ and you're done.
Questions, feedback, or contributions welcome.
What native handler would you find most useful with Nexus — camera access, biometrics, Bluetooth, local database? Let me know in the comments, it'll help prioritize what gets documented first.
Built by @fouzstack — open source Android libraries for the WebView ecosystem.
Top comments (0)