The app:web module is the Web application for NoteDelight, built with Compose Multiplatform for WebAssembly (Wasm). It provides a browser-based note-taking experience with the same UI and features as the native apps.
- Provide web application entry point
- Deploy as static website (GitHub Pages, Vercel, etc.)
- Support progressive web app (PWA) features
- Demonstrate Compose Multiplatform Web capabilities
- Enable browser-based note-taking without installation
app:web (Web Application - Wasm)
├── src/
│ ├── wasmJsMain/
│ │ ├── kotlin/
│ │ │ └── com/softartdev/notedelight/
│ │ │ └── main.kt # Web entry point
│ │ └── resources/
│ │ ├── index.html # HTML page
│ │ └── styles.css # Custom styles
│ └── wasmJsTest/
│ └── kotlin/ # Web tests (future)
├── build.gradle.kts # Build configuration
└── webpack.config.d/
└── sqljs-config.js # SQL.js webpack config
Web application entry point:
fun main() {
// Initialize Koin
startKoin {
modules(webModule)
}
// Render Compose app to HTML canvas
CanvasBasedWindow(canvasElementId = "ComposeTarget") {
App() // Shared Compose UI
}
}HTML entry point:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Note Delight</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<canvas id="ComposeTarget"></canvas>
<script src="web.js"></script>
</body>
</html>- Kotlin/Wasm: Compiles Kotlin to WebAssembly
- Performance: Near-native performance in browsers
- Size: Compact binary format
- Security: Sandboxed execution environment
- Official SQLite WASM: Native SQLite compiled to WebAssembly
- OPFS Storage: Origin-Private FileSystem for persistent database storage
- Web Worker: Database operations run off the main thread
- No encryption: SQLCipher not available in browsers
- Module bundler: Bundles Wasm, JS, and resources
- Development server: Hot reload during development
- Production build: Optimized bundles
# Run development server with hot reload
./gradlew :app:web:wasmJsBrowserDevelopmentRun --continuousThis starts a webpack dev server at http://localhost:8080
# Build optimized production bundle
./gradlew :app:web:wasmJsBrowserProductionWebpackOutput: app/web/build/dist/wasmJs/productionExecutable/
build/dist/wasmJs/productionExecutable/
├── index.html # Entry HTML
├── composeApp.js # JavaScript loader
├── composeApp.wasm # Application WebAssembly binary
├── skiko.wasm # Skia graphics engine
├── sqlite3.js # SQLite JavaScript
├── sqlite3.wasm # Official SQLite WASM
├── sqlite.worker.js # Custom OPFS worker
├── coi-serviceworker.js # Service worker for headers
└── sql-wasm.wasm # Legacy SQL.js (fallback)
The web app is a static site that can be hosted on:
# Build production
./gradlew :app:web:wasmJsBrowserProductionWebpack
# Deploy to gh-pages branch
cd build/dist/wasmJs/productionExecutable
git init
git add .
git commit -m "Deploy"
git push -f origin gh-pagesHosted at: https://username.github.io/NoteDelight/
# Install Vercel CLI
npm i -g vercel
# Build
./gradlew :app:web:wasmJsBrowserProductionWebpack
# Deploy
cd build/dist/wasmJs/productionExecutable
vercel --prod# Build
./gradlew :app:web:wasmJsBrowserProductionWebpack
# Deploy via Netlify CLI or drag-and-drop
netlify deploy --prod --dir=build/dist/wasmJs/productionExecutableAutomated deployment via GitHub Actions:
name: Deploy Web App
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: '17'
- name: Build Web App
run: ./gradlew :app:web:wasmJsBrowserProductionWebpack
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./app/web/build/dist/wasmJs/productionExecutableCustom webpack configuration in webpack.config.d/sqljs-config.js:
config.resolve = {
fallback: {
fs: false,
path: false,
crypto: false,
}
};
// Configure SQL.js
config.plugins.push(
new CopyWebpackPlugin({
patterns: [
{
from: '../sql-wasm.wasm',
to: '.'
}
]
})
);kotlin {
wasmJs {
moduleName = "web"
browser {
commonWebpackConfig {
outputFileName = "web.js"
}
}
binaries.executable()
}
}
compose.experimental {
web.application {}
}- Chrome/Edge: 119+ (Wasm GC support)
- Firefox: 120+ (Wasm GC support)
- Safari: 17.4+ (Wasm GC support)
fun isWasmSupported(): Boolean = js("""
(function() {
try {
if (typeof WebAssembly === "object" &&
typeof WebAssembly.instantiate === "function") {
return true;
}
} catch (e) {}
return false;
})()
""") as Boolean- ❌ No encryption: SQLCipher not available in browsers
- ✅ Storage: OPFS provides persistent database storage
⚠️ File access: Restricted browser file API⚠️ Performance: Slower than native (improving)⚠️ Binary size: Larger initial download than native apps⚠️ Experimental: Wasm support still evolving
- Files: Use File System Access API when available
- Performance: Lazy loading, code splitting
- Size: Compression, caching, CDN
The web app now uses OPFS (Origin-Private FileSystem) for persistent database storage, providing better performance and reliability than IndexedDB.
- Secure context: HTTPS or localhost
- Cross-Origin headers: Automatically configured
Cross-Origin-Embedder-Policy: require-corpCross-Origin-Opener-Policy: same-origin
- ✅ Persistent storage: Survives browser sessions
- ✅ Better performance: Direct file system access
- ✅ Larger capacity: Not limited by IndexedDB quotas
- ✅ Real SQLite: Uses official SQLite WASM build
- Chrome/Edge: 86+ (OPFS support)
- Firefox: 111+ (OPFS support)
- Safari: 15.2+ (OPFS support)
For deployment on static hosts like GitHub Pages, a service worker (coi-serviceworker.js) automatically enables the required headers.
The web app supports dynamic locale switching through Compose Multiplatform resources:
- Locale Override: JavaScript code in
index.htmloverridesnavigator.languagesto support custom locale selection - Locale Storage: The
LocaleInteractor.wasmJsimplementation useswindow.__customLocaleto persist locale preferences - Supported Languages: English and Russian
The locale override script must be loaded before composeApp.js to ensure proper initialization. The script checks for window.__customLocale and returns it when set, otherwise falls back to the default navigator.languages behavior.
Enable offline support:
// sw.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('note-delight-v1').then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/web.js',
'/web.wasm',
'/styles.css'
]);
})
);
});{
"name": "Note Delight",
"short_name": "NoteDelight",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#6200ee",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}core:domain- Domain layercore:data:db-sqldelight- Data layer (with sql.js)core:presentation- ViewModelsui:shared- Shared UI
compose.web.application- Compose for Webcompose.runtime- Compose runtime
kotlinx.browser- Browser API bindings
sqlDelight.web- SQLDelight Web driver- NPM:
sql.js- SQLite for browsers - NPM:
@cashapp/sqldelight-sqljs-worker- Web Worker support
The web app includes multiplatform Compose UI tests that extend CommonUiTests from the ui/test module:
Location: app/web/src/wasmJsTest/kotlin/WebUiTests.kt
Test Coverage:
- CRUD operations
- Title editing after create/save
- Database prepopulation
- Encryption flow
- Password settings
- Locale switching
Running Tests:
# Requires CHROME_BIN environment variable
export CHROME_BIN=/path/to/chrome
./gradlew :app:web:wasmJsBrowserTestTest Configuration:
- Uses Karma with Chrome headless for test execution
- Automatically disabled if
CHROME_BINis not set - Tests use SQL.js fallback when SQLite3 WASM is not available (common in headless browsers)
- Database is automatically cleaned up before each test
Database Fallback for Tests:
The test worker (sqlite.worker.js) implements a fallback mechanism:
- First attempts to use official SQLite3 WASM with OPFS support
- Falls back to SQL.js (in-memory) if SQLite3 is unavailable
- This ensures tests can run in headless browser environments that may not support OPFS
Web-specific unit tests:
// In wasmJsTest/
@Test
fun testWebInitialization() {
// Test web-specific initialization
}# Unit tests
./gradlew :app:web:wasmJsTest
# UI tests (requires CHROME_BIN)
export CHROME_BIN=/path/to/chrome
./gradlew :app:web:wasmJsBrowserTestManual testing in browsers:
# Start dev server
./gradlew :app:web:wasmJsBrowserDevelopmentRun --continuous
# Open in browser
open http://localhost:8080// Lazy load heavy features
val heavyFeature by lazy {
loadHeavyFeature()
}Enable compression in server config:
# Nginx
gzip on;
gzip_types application/wasm application/javascript;Configure cache headers:
Cache-Control: public, max-age=31536000, immutable
When working with this module:
- Browser APIs: Use
kotlinx.browserfor DOM access - No native code: Everything must work in browser sandbox
- Size matters: Minimize bundle size
- Progressive enhancement: Detect and use modern APIs gracefully
- Testing: Test in multiple browsers
- Security: No sensitive data without encryption
- Performance: Profile and optimize load time
- Responsive: Support mobile and desktop browsers
- Accessibility: Follow WCAG guidelines
- PWA: Consider offline functionality
// Lazy load resources
val image by lazy {
loadResource("image.png")
}// Use localStorage for settings
fun saveSettings(settings: Settings) {
window.localStorage.setItem(
"settings",
JSON.stringify(settings)
)
}try {
// Browser operation
} catch (e: dynamic) {
console.error("Browser error:", e)
showUserFriendlyError()
}- Wasm not found: Update Kotlin and Compose plugins
- Webpack errors: Check
webpack.config.d/configuration - Missing dependencies: Run
./gradlew --refresh-dependencies
- White screen: Check browser console for errors
- Database errors: Verify sql.js is loaded
- Slow performance: Profile with browser DevTools
- Check Wasm GC support: Use modern browser versions
- Polyfills: Not available for Wasm GC
- Fallback: Provide message for unsupported browsers
- Depends on:
ui:shared,core:presentation,core:data:db-sqldelight,core:domain - Alternative apps:
app:android,app:desktop,app:iosApp