Building desktop apps and PWAs
Build cross-platform desktop applications and Progressive Web Apps with Aurelia using Electron, Tauri, and modern PWA technologies.
Deploy your Aurelia application beyond the browser—as installable Progressive Web Apps (PWAs) that work offline, or as native desktop applications for Windows, macOS, and Linux using Electron or Tauri. This guide covers strategies for building, packaging, and distributing cross-platform applications.
Why This Is an Advanced Scenario
Desktop and PWA deployment involves:
Platform adaptation - Tailoring UX for desktop vs. web
Native integrations - File system, system tray, notifications
Distribution complexity - App stores, auto-updates, code signing
Security considerations - Sandbox escaping, IPC security
Performance optimization - Bundle size, startup time, memory usage
Offline capabilities - Service workers, local data, sync strategies
Multi-platform testing - Windows, macOS, Linux variations
Advanced topics:
Custom protocols and deep linking
Native module integration
Hardware access (USB, Bluetooth, Serial)
System-level permissions
Crash reporting and analytics
Auto-update mechanisms
Technology Overview
Progressive Web Apps (PWAs)
Web applications with native-like capabilities:
Pros: No installation friction, automatic updates, cross-platform
Cons: Limited system access, browser dependency
Best for: Customer-facing apps, content-heavy applications
Electron
Chromium + Node.js for desktop apps:
Pros: Full Node.js access, mature ecosystem, extensive APIs
Cons: Large bundle size (~150MB), slower startup
Best for: Feature-rich applications, enterprise software
Tauri
Rust + WebView for lightweight desktop apps:
Pros: Tiny bundles (~5MB), fast startup, memory efficient, secure
Cons: Younger ecosystem, less plugins
Best for: Performant apps, resource-conscious deployments
Part 1: Progressive Web Apps (PWAs)
Complete PWA Guide
For comprehensive PWA documentation including service workers, caching strategies, manifest configuration, and offline functionality:
See the complete guide: Progressive Web Apps (PWA)
PWA Quick Start
npm install -D vite-plugin-pwa// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
aurelia(),
VitePWA({
registerType: 'autoUpdate',
manifest: {
name: 'Aurelia App',
short_name: 'Aurelia',
theme_color: '#814c9e',
icons: [
{
src: 'icon-192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'icon-512.png',
sizes: '512x512',
type: 'image/png'
}
]
}
})
]
});PWA Best Practices
Offline-First Architecture
App-like Navigation - No URL bar, custom chrome
Background Sync - Queue actions when offline
Push Notifications - Re-engagement strategy
Install Prompts - Strategic timing for install banners
Part 2: Electron Desktop Apps
Installing Electron
npm install --save-dev electron electron-builder
npm install --save-dev concurrently wait-on cross-envProject Structure
my-aurelia-electron-app/
├── src/ # Aurelia application
├── electron/
│ ├── main.js # Electron main process
│ ├── preload.js # Secure IPC bridge
│ └── menu.js # Application menu
├── dist/ # Built Aurelia app
├── build/ # Electron build output
├── package.json
└── electron-builder.yml # Distribution configMain Process Setup
// electron/main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const isDev = process.env.NODE_ENV === 'development';
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false, // Security best practice
enableRemoteModule: false
},
title: 'Aurelia Desktop App',
icon: path.join(__dirname, '../build/icon.png')
});
// Load Aurelia app
if (isDev) {
mainWindow.loadURL('http://localhost:3000');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
}
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// IPC handlers
ipcMain.handle('get-app-version', () => {
return app.getVersion();
});
ipcMain.handle('read-file', async (event, filePath) => {
const fs = require('fs').promises;
return await fs.readFile(filePath, 'utf-8');
});Preload Script (Secure IPC)
// electron/preload.js
const { contextBridge, ipcRenderer } = require('electron');
// Expose safe APIs to renderer
contextBridge.exposeInMainWorld('electronAPI', {
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),
onUpdateAvailable: (callback) => ipcRenderer.on('update-available', callback),
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
});Using Electron APIs in Aurelia
// src/services/electron-service.ts
export class ElectronService {
private api: any;
constructor() {
this.api = (window as any).electronAPI;
}
get isElectron(): boolean {
return !!this.api;
}
async getVersion(): Promise<string> {
if (!this.isElectron) return 'web';
return await this.api.getAppVersion();
}
async readFile(path: string): Promise<string> {
if (!this.isElectron) {
throw new Error('File system access requires Electron');
}
return await this.api.readFile(path);
}
}// src/my-component.ts
import { resolve } from '@aurelia/kernel';
import { ElectronService } from './services/electron-service';
export class MyComponent {
private electron = resolve(ElectronService);
version = '';
async attached() {
if (this.electron.isElectron) {
this.version = await this.electron.getVersion();
}
}
}Package.json Scripts
{
"scripts": {
"dev": "vite",
"build": "vite build",
"electron": "electron .",
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:3000 && cross-env NODE_ENV=development electron .\"",
"electron:build": "npm run build && electron-builder"
}
}Electron Builder Configuration
# electron-builder.yml
appId: com.yourcompany.aureliaapp
productName: Aurelia Desktop App
directories:
output: release
buildResources: build
files:
- dist/**/*
- electron/**/*
- package.json
mac:
category: public.app-category.productivity
icon: build/icon.icns
target:
- dmg
- zip
win:
icon: build/icon.ico
target:
- nsis
- portable
linux:
icon: build/icon.png
category: Office
target:
- AppImage
- deb
- rpmAuto-Updates
// electron/main.js
const { autoUpdater } = require('electron-updater');
app.whenReady().then(() => {
createWindow();
// Check for updates
autoUpdater.checkForUpdatesAndNotify();
autoUpdater.on('update-available', () => {
mainWindow.webContents.send('update-available');
});
autoUpdater.on('update-downloaded', () => {
mainWindow.webContents.send('update-ready');
});
});
ipcMain.handle('install-update', () => {
autoUpdater.quitAndInstall();
});Part 3: Tauri Desktop Apps
Installing Tauri
npm install --save-dev @tauri-apps/cli
npm install @tauri-apps/apiInitialize Tauri:
npx tauri initProject Structure
my-aurelia-tauri-app/
├── src/ # Aurelia application
├── src-tauri/
│ ├── src/
│ │ └── main.rs # Rust backend
│ ├── tauri.conf.json # Tauri configuration
│ ├── Cargo.toml # Rust dependencies
│ └── icons/ # App icons
├── dist/ # Built Aurelia app
└── package.jsonTauri Configuration
// src-tauri/tauri.conf.json
{
"build": {
"distDir": "../dist",
"devPath": "http://localhost:3000",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"package": {
"productName": "Aurelia Desktop App",
"version": "1.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"fs": {
"readFile": true,
"writeFile": true,
"scope": ["$APPDATA/*"]
},
"dialog": {
"open": true,
"save": true
},
"shell": {
"open": true
}
},
"windows": [
{
"title": "Aurelia Desktop App",
"width": 1200,
"height": 800,
"resizable": true,
"fullscreen": false
}
]
}
}Rust Backend (main.rs)
// src-tauri/src/main.rs
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use tauri::Manager;
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[tauri::command]
async fn process_data(data: Vec<i32>) -> Result<Vec<i32>, String> {
// Expensive computation in Rust
Ok(data.iter().map(|x| x * 2).collect())
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, process_data])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Using Tauri APIs in Aurelia
// src/services/tauri-service.ts
import { invoke } from '@tauri-apps/api/tauri';
import { open } from '@tauri-apps/api/dialog';
import { writeTextFile, readTextFile } from '@tauri-apps/api/fs';
export class TauriService {
async greet(name: string): Promise<string> {
return await invoke('greet', { name });
}
async processData(data: number[]): Promise<number[]> {
return await invoke('process_data', { data });
}
async openFile(): Promise<string | null> {
const selected = await open({
multiple: false,
filters: [{
name: 'Text',
extensions: ['txt', 'md']
}]
});
if (selected && typeof selected === 'string') {
return await readTextFile(selected);
}
return null;
}
async saveFile(content: string, path: string): Promise<void> {
await writeTextFile(path, content);
}
}// src/my-component.ts
import { resolve } from '@aurelia/kernel';
import { TauriService } from './services/tauri-service';
export class MyComponent {
private tauri = resolve(TauriService);
greeting = '';
async attached() {
this.greeting = await this.tauri.greet('Aurelia User');
}
async handleOpenFile() {
const content = await this.tauri.openFile();
if (content) {
console.log('File content:', content);
}
}
}Package.json Scripts
{
"scripts": {
"dev": "vite",
"build": "vite build",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
}
}Building for Distribution
# Development
npm run tauri:dev
# Production builds
npm run tauri:buildOutputs:
Windows:
.exeinstaller,.msimacOS:
.app,.dmgLinux:
.AppImage,.deb,.rpm
Comparison: Electron vs. Tauri
Bundle Size
~150MB
~5MB
Memory
100-200MB base
30-50MB base
Startup Time
1-3 seconds
<1 second
Backend Language
Node.js (JavaScript)
Rust
Maturity
Very mature
Growing
Plugin Ecosystem
Extensive
Growing
Security
Good (with care)
Excellent
Auto-Updates
Built-in
Third-party
Learning Curve
JavaScript only
Requires Rust
Common Patterns
Environment Detection
export class PlatformService {
get isElectron(): boolean {
return !!(window as any).electronAPI;
}
get isTauri(): boolean {
return !!(window as any).__TAURI__;
}
get isPWA(): boolean {
return window.matchMedia('(display-mode: standalone)').matches;
}
get isWeb(): boolean {
return !this.isElectron && !this.isTauri && !this.isPWA;
}
get platform(): 'electron' | 'tauri' | 'pwa' | 'web' {
if (this.isElectron) return 'electron';
if (this.isTauri) return 'tauri';
if (this.isPWA) return 'pwa';
return 'web';
}
}Cross-Platform File Operations
export class FileService {
private platform = resolve(PlatformService);
private electron = resolve(ElectronService);
private tauri = resolve(TauriService);
async readFile(path: string): Promise<string> {
switch (this.platform.platform) {
case 'electron':
return this.electron.readFile(path);
case 'tauri':
return this.tauri.readTextFile(path);
case 'pwa':
case 'web':
throw new Error('File system access not available in browser');
}
}
}System Notifications
// Works across all platforms
export class NotificationService {
async notify(title: string, body: string) {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(title, { body });
} else if ('Notification' in window) {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
new Notification(title, { body });
}
}
}
}Distribution & Deployment
Code Signing
macOS:
# Electron
electron-builder --mac --publish neverWindows:
# Requires code signing certificate
electron-builder --win --publish neverApp Stores
Microsoft Store: Package as MSIX
Mac App Store: Notarization required
Snap Store (Linux): Snapcraft packaging
Auto-Updates
Electron + electron-updater:
const server = 'https://your-update-server.com';
const feed = `${server}/update/${process.platform}/${app.getVersion()}`;
autoUpdater.setFeedURL(feed);Tauri: Use external update servers or GitHub releases
Security Best Practices
Disable Node Integration (Electron)
Enable Context Isolation (Electron)
Use Preload Scripts for IPC
Validate All IPC Messages
Allowlist Required APIs (Tauri)
Keep Dependencies Updated
Implement CSP Headers
Code Sign All Releases
Performance Optimization
Bundle Size
Tree-shake unused code
Code split large dependencies
Compress assets
Use native modules sparingly
Startup Time
Lazy load heavy components
Defer non-critical initialization
Cache computed values
Optimize Electron/Tauri window creation
Memory Usage
Dispose subscriptions properly
Use virtual scrolling for lists
Implement pagination
Profile with DevTools
Testing Desktop Apps
// src/services/__tests__/electron-service.spec.ts
import { ElectronService } from '../electron-service';
describe('ElectronService', () => {
let service: ElectronService;
beforeEach(() => {
// Mock Electron API
(window as any).electronAPI = {
getAppVersion: () => Promise.resolve('1.0.0'),
readFile: (path: string) => Promise.resolve('mock content')
};
service = new ElectronService();
});
it('detects Electron environment', () => {
expect(service.isElectron).toBe(true);
});
it('gets app version', async () => {
const version = await service.getVersion();
expect(version).toBe('1.0.0');
});
});Resources
PWA
Electron
Tauri
Deployment
Last updated
Was this helpful?