Skip to content

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

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

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

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:

  1. Frontend calls a Go method (via auto-generated binding)
  2. Bridge encodes the call to JSON (method name + arguments)
  3. Router finds the Go method in registered services
  4. Go method executes and returns a value
  5. Bridge decodes the result and sends back to frontend
  6. 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

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 available
func (g *GreetService) Greet(name string) string {
return g.prefix + name + "!"
}
func (g *GreetService) GetTime() time.Time {
return time.Now()
}
// Register the service
app := 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.ts
export 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

Learn more about services →

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 event
app.Event.Emit("user-logged-in", user)
// JavaScript: Listen for event
import { Events } from '@wailsio/runtime'
Events.On('user-logged-in', (user) => {
console.log('User logged in:', user)
})

Learn more about events →

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.Create
Init.Create -> Init.Register
Init.Register -> Init.Setup
Init.Setup -> Run.Events
Run.Events -> Run.Messages
Run.Messages -> Run.Render
Run.Render -> Run.Events: "Loop"
Run.Events -> Shutdown.Cleanup: "Quit signal"
Shutdown.Cleanup -> Shutdown.Save
Shutdown.Save -> End

Lifecycle 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().

Learn more about lifecycle →

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.AnalyseGo
Build.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.CompileGo
Build.CompileGo -> Build.Embed
Build.Embed -> Output.Binary

Build steps:

  1. Analyse Go code

    • Scan services for exported methods
    • Extract parameter and return types
    • Generate method signatures
  2. Generate TypeScript bindings

    • Create .ts files for each service
    • Include full type definitions
    • Add JSDoc comments
  3. Build frontend

    • Run your bundler (Vite, webpack, etc.)
    • Minify and optimise
    • Output to frontend/dist/
  4. Compile Go

    • Compile with optimisations (-ldflags="-s -w")
    • Include build metadata
    • Platform-specific compilation
  5. Embed assets

    • Embed frontend files into Go binary
    • Compress assets
    • Create single executable

Result: A single native executable with everything embedded.

Learn more about building →

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

Understanding memory usage helps you build efficient applications.

Memory regions:

  1. Go Heap

    • Your services and application state
    • Managed by Go garbage collector
    • Typically 5-10MB for simple apps
  2. WebView Memory

    • DOM, JavaScript heap, CSS
    • Managed by WebView’s engine
    • Typically 10-20MB for simple apps
  3. 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 →

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:

  1. Method whitelisting

    • Only exported methods are callable
    • Private methods are inaccessible
    • Explicit service registration required
  2. Type validation

    • Arguments checked against Go types
    • Invalid types rejected
    • Prevents injection attacks
  3. No eval()

    • Frontend can’t execute arbitrary Go code
    • Only predefined methods callable
    • No dynamic code execution
  4. 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

Learn more about security →

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.