Quick Start
Get started with Wails v3.
Wails v3 is a complete rewrite with significant improvements in architecture, performance, and developer experience. This guide helps you migrate your v2 application to v3.
Key changes:
Migration time: 1-4 hours for typical applications
The CLI can perform most of this guide for you:
wails3 migrate -d ./myv2project -o ./myv3projectThe command migrates what maps deterministically and documents the rest. It does not rewrite your application logic and it does not generate compatibility layers: code that still uses the v2 API is left untouched, and every such location is listed in a generated MIGRATION.md with its concrete v3 replacement, so the remaining work is a clear checklist rather than a half-migrated codebase.
What it migrates for you:
main.go is rewritten around application.New() + app.Window.NewWithOptions(), keeping your own code and comments intact. Options are mapped to their v3 equivalents, including platform-specific window options.Bind become v3 services, and the OnStartup/OnDomReady/OnShutdown/OnBeforeClose callbacks are wired to their v3 counterparts (application events, OnShutdown, ShouldQuit).wails.json is replaced by the v3 project files: a Taskfile-based build system and build/config.yml populated from your v2 metadata (product info, file associations, protocols).go.mod swaps wails/v2 for wails/v3; everything else is preserved.@wailsio/runtime is added to its dependencies. The generated wailsjs/ directory is not carried over - it is v2 build output that cannot work with v3.What it documents for you (in MIGRATION.md):
runtime package, listed by file and line with the v3 replacement (for example runtime.EventsEmit(ctx, ...) becomes app.Event.Emit(...)). The project intentionally does not compile until these are ported - the compiler points at exactly the listed locations.wailsjs/runtime or wailsjs/go/..., with the @wailsio/runtime equivalent and the wails3 generate bindings workflow for bindings.EnumBind, …), each with instructions.The rest of this guide explains the underlying changes in depth - use it together with the generated checklist.
In v2, application setup, window configuration, and execution were all combined into a single wails.Run() call. This monolithic approach made it difficult to create multiple windows, handle errors at different stages, or test individual components of your application.
v3 separates these concerns into distinct phases: application creation, window creation, and execution. This separation gives you explicit control over each stage of your application’s lifecycle and makes the code more modular and testable.
v2:
err := wails.Run(&options.App{ Title: "My App", Width: 1024, Height: 768, Bind: []interface{}{ &GreetService{}, },})v3:
app := application.New(application.Options{ Name: "My App", Services: []application.Service{ application.NewService(&GreetService{}), },})
window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "My App", Width: 1024, Height: 768,})
app.Run()Why this is better:
In v2, every bound struct required a context field and a startup(ctx) method to receive the runtime context. This created tight coupling between your business logic and the Wails runtime, making code harder to test and understand.
v3 introduces the service pattern, where your structs are completely standalone and don’t need to store runtime context. If a service needs access to the application instance, it explicitly receives it through dependency injection rather than implicit context threading.
v2:
type App struct { ctx context.Context}
func (a *App) startup(ctx context.Context) { a.ctx = ctx}
func (a *App) Greet(name string) string { return "Hello " + name}v3:
type GreetService struct{}
func (g *GreetService) Greet(name string) string { return "Hello " + name}
// Register as serviceapp := application.New(application.Options{ Services: []application.Service{ application.NewService(&GreetService{}), },})Why this is better:
App structServiceStartup() method when you need initialization, making it explicitIn v2, all runtime operations required passing a context to global functions from the runtime package. This created tight coupling to the context object throughout your codebase and made the API feel procedural rather than object-oriented.
v3 replaces the context-based runtime with direct method calls on application and window objects. Operations are called directly on the objects they affect, making the code more intuitive and object-oriented.
v2:
import "github.com/wailsapp/wails/v2/pkg/runtime"
runtime.WindowSetTitle(a.ctx, "New Title")runtime.EventsEmit(a.ctx, "event-name", data)v3:
// Store app referencetype MyService struct { app *application.App}
func (s *MyService) UpdateTitle() { window := s.app.Window.Current() window.SetTitle("New Title")}
func (s *MyService) EmitEvent() { s.app.Event.Emit("event-name", data)}Why this is better:
window.SetTitle() is more obvious than runtime.WindowSetTitle(ctx, ...)In v2, bindings were organized by Go package and struct name, typically resulting in paths like wailsjs/go/main/App. This structure didn’t reflect logical grouping and made it hard to find related functionality.
v3 organizes bindings by service name and application module, creating a clearer logical structure. The bindings are generated into a bindings directory organized by your application name and service names, making it easier to understand what functionality is available.
v2:
import { Greet } from '../wailsjs/go/main/App'
const result = await Greet("World")v3:
import { Greet } from './bindings/changeme/greetservice'
const result = await Greet("World")Why this is better:
../wailsjs/go prefix - just ./bindingsIn v2, events used variadic interface{} parameters and required passing context to every event function. Event handlers received untyped data that needed manual type assertions, making the event system error-prone and hard to debug.
v3 introduces typed event objects and removes the context requirement. Event handlers receive a proper event object with typed data, making the event system more reliable and easier to use.
v2:
runtime.EventsOn(ctx, "event-name", func(data ...interface{}) { // Handle event})
runtime.EventsEmit(ctx, "event-name", data)v3:
app.Event.On("event-name", func(e *application.CustomEvent) { data := e.Data // Handle event})
app.Event.Emit("event-name", data)Why this is better:
...interface{}app.Event.On() and app.Event.Emit() are more intuitive than runtime functionsv2 supported only a single window per application. The window was created at startup and all window operations were performed through runtime functions that implicitly targeted that single window.
v3 introduces native multi-window support as a core feature. Each window is a first-class object with its own methods and lifecycle. You can create, manage, and destroy multiple windows dynamically throughout your application’s lifetime.
v2:
// Single window onlyruntime.WindowSetSize(ctx, 800, 600)v3:
// Multiple windows supportedwindow1 := app.Window.New()window1.SetSize(800, 600)
window2 := app.Window.New()window2.SetSize(1024, 768)Why this is better:
go.mod:
module myapp
go 1.25.0
require ( github.com/wailsapp/wails/v3 v3.0.0-beta.0)Update:
go get github.com/wailsapp/wails/v3@latestgo mod tidyv2:
package main
import ( "embed" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/windows")
//go:embed all:frontend/distvar assets embed.FS
func main() { app := NewApp()
err := wails.Run(&options.App{ Title: "My App", Width: 1024, Height: 768, AssetServer: &assetserver.Options{ Assets: assets, }, Bind: []interface{}{ app, }, Windows: &windows.Options{ WebviewIsTransparent: false, }, })
if err != nil { println("Error:", err.Error()) }}v3:
package main
import ( "embed" "github.com/wailsapp/wails/v3/pkg/application")
//go:embed frontend/distvar assets embed.FS
func main() { app := application.New(application.Options{ Name: "My App", Services: []application.Service{ application.NewService(&MyService{}), }, Assets: application.AssetOptions{ Handler: application.AssetFileServerFS(assets), }, })
app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "My App", Width: 1024, Height: 768, })
err := app.Run() if err != nil { panic(err) }}v2:
type App struct { ctx context.Context}
func NewApp() *App { return &App{}}
func (a *App) startup(ctx context.Context) { a.ctx = ctx // Initialisation}
func (a *App) Greet(name string) string { return "Hello " + name}v3:
type MyService struct { app *application.App}
func NewMyService(app *application.App) *MyService { return &MyService{app: app}}
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { // Initialisation return nil}
func (s *MyService) Greet(name string) string { return "Hello " + name}
// Register after app creationapp := application.New(application.Options{})app.RegisterService(application.NewService(NewMyService(app)))v2:
func (a *App) DoSomething() { runtime.WindowSetTitle(a.ctx, "New Title") runtime.EventsEmit(a.ctx, "update", data) runtime.LogInfo(a.ctx, "Message")}v3:
func (s *MyService) DoSomething() { window := s.app.Window.Current() window.SetTitle("New Title")
s.app.Event.Emit("update", data)
s.app.Logger.Info("Message")}Generate new bindings:
wails3 generate bindingsUpdate imports:
// v2import { Greet } from '../wailsjs/go/main/App'
// v3import { Greet } from './bindings/changeme/myservice'Update event handling:
// v2import { EventsOn, EventsEmit } from '../wailsjs/runtime/runtime'
EventsOn("update", (data) => { console.log(data)})
EventsEmit("action", data)
// v3import { Events } from '@wailsio/runtime'
Events.On("update", (data) => { console.log(data)})
Events.Emit("action", data)v2 (wails.json):
{ "name": "myapp", "outputfilename": "myapp", "frontend:install": "npm install", "frontend:build": "npm run build", "frontend:dev:watcher": "npm run dev", "frontend:dev:serverUrl": "auto"}v3 (build/config.yml):
version: '3'
info: productName: "myapp" productIdentifier: "com.example.myapp"
dev_mode: root_path: .V3 project-level development and build commands are defined in the root
Taskfile.yml. The generated project keeps frontend commands there and uses
build/config.yml for application and packaging configuration. Do not create
a V3 wails.json by copying the V2 file.
v2:
selection, err := runtime.OpenFileDialog(ctx, runtime.OpenDialogOptions{ Title: "Select File",})v3:
selection, err := app.Dialog.OpenFileWithOptions(&application.OpenFileDialogOptions{ Title: "Select File",}).PromptForSingleSelection()v2:
menu := menu.NewMenu()menu.Append(menu.Text("File", nil, []*menu.MenuItem{ menu.Text("Quit", nil, func(_ *menu.CallbackData) { runtime.Quit(ctx) }),}))v3:
menu := app.NewMenu()fileMenu := menu.AddSubmenu("File")fileMenu.Add("Quit").OnClick(func(ctx *application.Context) { app.Quit()})v2:
// Not available in v2v3:
systray := app.SystemTray.New()systray.SetIcon(iconBytes)systray.SetLabel("My App")
menu := app.NewMenu()menu.Add("Show").OnClick(showWindow)menu.Add("Quit").OnClick(app.Quit)systray.SetMenu(menu)Problem: Import errors after migration
Solution:
# Regenerate bindingswails3 generate bindings
# Check output directoryls frontend/bindingsProblem: ctx not available
Solution:
Store app reference instead:
type MyService struct { app *application.App}
func NewMyService(app *application.App) *MyService { return &MyService{app: app}}Problem: runtime.WindowSetTitle() doesn’t exist
Solution:
Use window methods directly:
window := s.app.Window.Current()window.SetTitle("New Title")Problem: Events registered but not received
Solution:
Check event names match exactly:
// Goapp.Event.Emit("my-event", data)
// JavaScriptOnEvent("my-event", handler) // Must match exactly# Developmentwails3 dev
# Buildwails3 build
# Generate bindingswails3 generate bindingsMigration tool failures are bugs. Before opening an issue, reduce the problem to the smallest reproducible V2 project you can and include:
wails3 migrate command;MIGRATION.md;wails3 doctor output; andOpen the report in the Wails issue tracker using the bug report template. Do not attach secrets, signing credentials, or private source code.
If you want to propose a change to the migration behavior or V3 API, submit a Wails Enhancement Proposal as a pull request. Product proposals should not be filed as ordinary feature requests.
Q: Can I run v2 and v3 side by side?
A: Yes, they use different import paths.
Q: Is v3 production-ready?
A: v3 is beta software with a stable desktop API. Applications are running in
production with it, but test thoroughly before deployment. v2 remains the
current stable release.
Q: Will v2 be maintained?
A: Yes, v2 will receive critical updates.
Q: How long does migration take?
A: 1-4 hours for typical applications.
Quick Start
Get started with Wails v3.
Core Concepts
Understand v3 architecture.
Bindings
Learn the new bindings system.
Examples
See complete v3 examples.
Questions? Ask in Discord or open an issue.