How Wails Works
Wails is a framework for building desktop applications using Go for the backend and web technologies for the frontend. But unlike Electron, Wails doesn’t bundle a browser—it uses the operating system’s native WebView.
direction: left
Wails App: { shape: sequence_diagram label: "Wails App"
frontend: Frontend backend: Go Backend os: Operating System
Initialisation: { shape: sequence_diagram backend."Serves Static Web App" backend -> frontend: HTML / JS / CSS frontend."Render Site via OS-native WebView" } Regular Communication: { shape: sequence_diagram frontend."Make API-style call" frontend -> backend.a: JSON backend.a."Service processes request" backend.a -> os: Call System APIs backend.a."Generate Response" backend.a -> frontend: JSON frontend."Process response" }}Key differences from Electron:
| Aspect | Wails | Electron |
|---|---|---|
| Browser | OS-provided WebView | Bundled Chromium (~100MB) |
| Backend | Go (compiled) | Node.js (interpreted) |
| Communication | In-memory bridge | IPC (inter-process) |
| Bundle Size | ~15MB | ~150MB |
| Memory | ~10MB | ~100MB+ |
| Startup | <0.5s | 2-3s |
Core Components
Section titled “Core Components”1. Native WebView
Section titled “1. Native WebView”Wails uses the operating system’s built-in web rendering engine:
WebView2 (Microsoft Edge WebView2)
- Based on Chromium (same as Edge browser)
- Pre-installed on Windows 10/11
- Automatic updates via Windows Update
- Full modern web standards support
WebKit (Safari’s rendering engine)
- Built into macOS
- Same engine as Safari browser
- Excellent performance and battery life
- Full modern web standards support
WebKitGTK (GTK port of WebKit)
- Installed via package manager
- Same engine as GNOME Web (Epiphany)
- Good standards support
- Lightweight and performant
Why this matters:
- No bundled browser → Smaller app size
- OS-native → Better integration and performance
- Auto-updates → Security patches from OS updates
- Familiar rendering → Same as system browser
2. The Wails Bridge
Section titled “2. The Wails Bridge”The bridge is the heart of Wails—it enables direct communication between Go and JavaScript.
direction: down
Frontend: "Frontend (JavaScript)" { shape: rectangle style.fill: "#8B5CF6"}
Bridge: "Wails Bridge" { Encoder: "JSON Encoder" { shape: rectangle }
Router: "Method Router" { shape: diamond style.fill: "#10B981" }
Decoder: "JSON Decoder" { shape: rectangle }}
Backend: "Backend (Go)" { Services: "Registered Services" { shape: rectangle style.fill: "#00ADD8" }}
Frontend -> Bridge.Encoder: "1. Call Go method\nGreet('Alice')"Bridge.Encoder -> Bridge.Router: "2. Encode to JSON\n{method: 'Greet', args: ['Alice']}"Bridge.Router -> Backend.Services: "3. Route to service\nGreetService.Greet('Alice')"Backend.Services -> Bridge.Decoder: "4. Return result\n'Hello, Alice!'"Bridge.Decoder -> Frontend: "5. Decode to JS\nPromise resolves"How it works:
- Frontend calls a Go method (via auto-generated binding)
- Bridge encodes the call to JSON (method name + arguments)
- Router finds the Go method in registered services
- Go method executes and returns a value
- Bridge decodes the result and sends back to frontend
- Promise resolves in JavaScript with the result
Performance characteristics:
- In-memory: No network overhead, no HTTP
- Zero-copy where possible (for large data)
- Async by default: Non-blocking on both sides
- Type-safe: TypeScript definitions auto-generated
3. Service System
Section titled “3. Service System”Services are the recommended way to expose Go functionality to the frontend.
// Define a service (just a regular Go struct)type GreetService struct { prefix string}
// Methods with exported names are automatically availablefunc (g *GreetService) Greet(name string) string { return g.prefix + name + "!"}
func (g *GreetService) GetTime() time.Time { return time.Now()}
// Register the serviceapp := application.New(application.Options{ Services: []application.Service{ application.NewService(&GreetService{prefix: "Hello, "}), },})Service discovery:
- Wails scans your struct at startup
- Exported methods become callable from frontend
- Type information is extracted for TypeScript bindings
- Error handling is automatic (Go errors → JS exceptions)
Generated TypeScript binding:
// Auto-generated in frontend/bindings/GreetService.tsexport function Greet(name: string): Promise<string>export function GetTime(): Promise<Date>Why services?
- Type-safe: Full TypeScript support
- Auto-discovery: No manual registration of methods
- Organised: Group related functionality
- Testable: Services are just Go structs
4. Event System
Section titled “4. Event System”Events enable pub/sub communication between components.
direction: left
Wails Event System: { shape: sequence_diagram
window1: Window 1 window2: Window 2 backend: Go Backend
Event Driver: { shape: sequence_diagram window1."Subscribe to 'data-updated' events" window2."Subscribe to 'data-updated' events" backend.a."App Emit('data-updated', data)" backend.a -> window1.a:"JSON Event Bus" backend.a -> window2:"JSON Event Bus" window1.a."Subscriber processes On('data-updated', handler)" window2."Subscriber processes On('data-updated', handler)" }}Use cases:
- Window communication: One window notifies others
- Background tasks: Go service notifies UI of progress
- State synchronisation: Keep multiple windows in sync
- Loose coupling: Components don’t need direct references
Example:
// Go: Emit an eventapp.Event.Emit("user-logged-in", user)// JavaScript: Listen for eventimport { Events } from '@wailsio/runtime'
Events.On('user-logged-in', (user) => { console.log('User logged in:', user)})Application Lifecycle
Section titled “Application Lifecycle”Understanding the lifecycle helps you know when to initialise resources and clean up.
direction: down
Start: "Application Start" { shape: oval style.fill: "#10B981"}
Init: "Initialisation" { Create: "Create Application" { shape: rectangle }
Register: "Register Services" { shape: rectangle }
Setup: "Setup Windows/Menus" { shape: rectangle }}
Run: "Event Loop" { Events: "Process Events" { shape: rectangle }
Messages: "Handle Messages" { shape: rectangle }
Render: "Update UI" { shape: rectangle }}
Shutdown: "Shutdown" { Cleanup: "Cleanup Resources" { shape: rectangle }
Save: "Save State" { shape: rectangle }}
End: "Application End" { shape: oval style.fill: "#EF4444"}
Start -> Init.CreateInit.Create -> Init.RegisterInit.Register -> Init.SetupInit.Setup -> Run.EventsRun.Events -> Run.MessagesRun.Messages -> Run.RenderRun.Render -> Run.Events: "Loop"Run.Events -> Shutdown.Cleanup: "Quit signal"Shutdown.Cleanup -> Shutdown.SaveShutdown.Save -> EndLifecycle hooks:
app := application.New(application.Options{ Name: "My App",
// Cleanly intercept quit requests (e.g. unsaved changes). ShouldQuit: func() bool { return true },
// Called when the app is confirmed to be quitting — save state, close connections, etc. OnShutdown: func() {},})There is no OnStartup field on application.Options. Startup work belongs in a service’s ServiceStartup(ctx, options), in a callback registered via app.Event.OnApplicationEvent(events.Common.ApplicationStarted, ...), or simply before app.Run().
Build Process
Section titled “Build Process”Understanding how Wails builds your application:
direction: down
Source: "Source Code" { Go: "Go Code\n(main.go, services)" { shape: rectangle style.fill: "#00ADD8" }
Frontend: "Frontend Code\n(HTML/CSS/JS)" { shape: rectangle style.fill: "#8B5CF6" }}
Build: "Build Process" { AnalyseGo: "Analyse Go Code" { shape: rectangle }
GenerateBindings: "Generate Bindings" { shape: rectangle }
BuildFrontend: "Build Frontend" { shape: rectangle }
CompileGo: "Compile Go" { shape: rectangle }
Embed: "Embed Assets" { shape: rectangle }}
Output: "Output" { Binary: "Native Binary\n(myapp.exe/.app)" { shape: rectangle style.fill: "#10B981" }}
Source.Go -> Build.AnalyseGoBuild.AnalyseGo -> Build.GenerateBindings: "Extract types"Build.GenerateBindings -> Source.Frontend: "TypeScript bindings"Source.Frontend -> Build.BuildFrontend: "Compile (Vite/webpack)"Build.BuildFrontend -> Build.Embed: "Bundled assets"Source.Go -> Build.CompileGoBuild.CompileGo -> Build.EmbedBuild.Embed -> Output.BinaryBuild steps:
-
Analyse Go code
- Scan services for exported methods
- Extract parameter and return types
- Generate method signatures
-
Generate TypeScript bindings
- Create
.tsfiles for each service - Include full type definitions
- Add JSDoc comments
- Create
-
Build frontend
- Run your bundler (Vite, webpack, etc.)
- Minify and optimise
- Output to
frontend/dist/
-
Compile Go
- Compile with optimisations (
-ldflags="-s -w") - Include build metadata
- Platform-specific compilation
- Compile with optimisations (
-
Embed assets
- Embed frontend files into Go binary
- Compress assets
- Create single executable
Result: A single native executable with everything embedded.
Development vs Production
Section titled “Development vs Production”Wails behaves differently in development and production:
Characteristics:
- Hot reload: Frontend changes reload instantly
- Source maps: Debug with original source
- DevTools: Browser DevTools available
- Logging: Verbose logging enabled
- External frontend: Served from dev server (Vite)
How it works:
direction: right
WailsApp: "Wails App" { shape: rectangle style.fill: "#00ADD8"}
DevServer: "Vite Dev Server\n(localhost:5173)" { shape: rectangle style.fill: "#8B5CF6"}
WebView: "WebView" { shape: rectangle style.fill: "#6B7280"}
WailsApp -> DevServer: "Proxy requests"DevServer -> WebView: "Serve with HMR"WebView -> WailsApp: "Call Go methods"Benefits:
- Instant feedback on changes
- Full debugging capabilities
- Faster iteration
Characteristics:
- Embedded assets: Frontend built into binary
- Optimised: Minified, compressed
- No DevTools: Disabled by default
- Minimal logging: Errors only
- Single file: Everything in one executable
How it works:
direction: right
Binary: "Single Binary\n(myapp.exe)" { GoCode: "Compiled Go" { shape: rectangle style.fill: "#00ADD8" }
Assets: "Embedded Assets\n(HTML/CSS/JS)" { shape: rectangle style.fill: "#8B5CF6" }}
WebView: "WebView" { shape: rectangle style.fill: "#6B7280"}
Binary.Assets -> WebView: "Serve from memory"WebView -> Binary.GoCode: "Call Go methods"Benefits:
- Single file distribution
- Smaller size (minified)
- Better performance
- No external dependencies
Memory Model
Section titled “Memory Model”Understanding memory usage helps you build efficient applications.
Memory regions:
-
Go Heap
- Your services and application state
- Managed by Go garbage collector
- Typically 5-10MB for simple apps
-
WebView Memory
- DOM, JavaScript heap, CSS
- Managed by WebView’s engine
- Typically 10-20MB for simple apps
-
Bridge Memory
- Message buffers for communication
- Minimal overhead (<1MB)
- Zero-copy for large data where possible
Optimisation tips:
- Avoid large data transfers: Pass IDs, fetch details on demand
- Use events for updates: Don’t poll from frontend
- Stream large files: Don’t load entirely into memory
- Clean up listeners: Remove event listeners when done
Learn more about performance →
Security Model
Section titled “Security Model”Wails provides a secure-by-default architecture:
direction: down
Frontend: "Frontend (Untrusted)" { shape: rectangle style.fill: "#EF4444"}
Bridge: "Wails Bridge (Validation)" { shape: diamond style.fill: "#F59E0B"}
Backend: "Backend (Trusted)" { shape: rectangle style.fill: "#10B981"}
Frontend -> Bridge: "Call method"Bridge -> Bridge: "Validate:\n- Method exists?\n- Types correct?\n- Access allowed?"Bridge -> Backend: "Execute if valid"Backend -> Bridge: "Return result"Bridge -> Frontend: "Send response"Security features:
-
Method whitelisting
- Only exported methods are callable
- Private methods are inaccessible
- Explicit service registration required
-
Type validation
- Arguments checked against Go types
- Invalid types rejected
- Prevents injection attacks
-
No eval()
- Frontend can’t execute arbitrary Go code
- Only predefined methods callable
- No dynamic code execution
-
Context isolation
- Each window has its own context
- Services can check caller context
- Permissions per window possible
Best practices:
- Validate user input in Go (don’t trust frontend)
- Use context for authentication/authorisation
- Sanitise file paths before file operations
- Rate limit expensive operations
Next Steps
Section titled “Next Steps”Application Lifecycle - Understand startup, shutdown, and lifecycle hooks
Learn More →
Go-Frontend Bridge - Deep dive into how the bridge works
Learn More →
Build System - Understand how Wails builds your application
Learn More →
Start Building - Apply what you’ve learned in a tutorial Tutorials →
Questions about architecture? Ask in Discord or check the API reference.