| name | wails3 |
| description | Conventions for building Go desktop apps with Wails v3 — services, TypeScript bindings, events, windows, and packaging. Use when writing Wails v3 code, when the user says "Wails", "wails3", or "Go desktop app", or when the repo contains a Taskfile.yml alongside go.mod importing github.com/wailsapp/wails/v3 or a frontend using @wailsio/runtime. |
Go + Wails v3
Wails v3 is alpha. APIs shift between releases — when a call below doesn't compile, check the docs for the version pinned in go.mod (go list -m github.com/wailsapp/wails/v3) and the examples shipped in the module (v3/examples/), rather than forcing the pattern. Never apply v2 documentation to a v3 project.
Architecture: application + services + windows
v3 replaced v2's single bound App struct with an application object, any number of windows, and plain Go structs registered as services:
package main
import (
"embed"
"github.com/wailsapp/wails/v3/pkg/application"
)
var assets embed.FS
func main() {
app := application.New(application.Options{
Name: "myapp",
Services: []application.Service{
application.NewService(&GreetService{}),
},
Assets: application.AssetOptions{Handler: application.AssetFileServerFS(assets)},
})
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Main", Width: 900, Height: 600, URL: "/",
})
if err := app.Run(); err != nil {
panic(err)
}
}
- Multiple windows are first-class: create more with
app.Window.NewWithOptions at any time, each with its own URL/options. (Older alphas expose this as app.NewWebviewWindow* — match the installed version.)
- A service is any struct: exported methods become callable from JS. Two sane shapes: several domain services (
FileService, SettingsService), or a single thin facade service delegating to internal/ domain packages — the facade keeps binding surface small while real logic stays in ordinary Go packages. Either way, business logic lives in internal/, not in bound methods.
- Services can hook lifecycle by implementing
ServiceStartup(ctx context.Context, options application.ServiceOptions) error and ServiceShutdown() error — do init/teardown there, not in main.
- v2 patterns that are gone:
context.Context as the first argument of bound methods, the runtime package (runtime.EventsEmit, runtime.WindowSetTitle, …), wails.json-driven builds. Window/dialog/event capabilities now live on app and window objects; a service that needs them can take the *application.App in its constructor or use application.Get().
Bindings: Go → TypeScript
Frontend calls Go through generated typed bindings, not strings:
wails3 generate bindings
import { Greet } from "./bindings/myapp/greetservice";
const msg = await Greet("World");
Rules:
- Regenerate bindings after every change to a bound method's signature or its parameter/return types. Stale bindings fail at runtime, not compile time.
wails3 dev regenerates on Go changes; manual builds need the generate step.
- Method parameters/returns must be JSON-serializable; structs become generated TS models. Errors returned from Go reject the Promise.
- Bound methods run concurrently on their own goroutines — guard shared service state with mutexes. A slow method only stalls its own Promise, but never do UI/main-thread work directly inside one; use
application.InvokeSync/InvokeAsync for main-thread-only operations, and never block the main thread with long-running work (it freezes every window).
Events: Go ↔ JS
app.Event.Emit("download:progress", 42)
app.Event.On("frontend:ready", func(e *application.CustomEvent) { … })
import { Events } from "@wailsio/runtime";
Events.On("download:progress", (ev) => setProgress(ev.data));
Events.Emit("frontend:ready");
Use request/response via bound methods; use events for server-push (progress, watchers, background job status). Namespace event names (domain:action).
Read references/services-and-events.md when wiring a real service — complete example with lifecycle, bindings output, progress events, and cleanup.
Frontend is just a web app
The frontend is any stack served from frontend/ — Vite + React/Svelte/Solid/vanilla all work; the other skills in this repo apply unchanged to it. Wails specifics are only: call Go via generated bindings, listen via @wailsio/runtime Events, and build output goes where the Assets embed expects (frontend/dist by convention).
Dev and build workflow
v3 builds are Taskfile-based (Taskfile.yml at the repo root wraps everything; there is no wails.json build config):
wails3 dev — hot-reloading dev mode (frontend dev server + Go rebuild on change).
wails3 build — production binary for the current OS.
wails3 package — installable artifact per OS (.app/.dmg, NSIS installer, AppImage/deb). Cross-compilation of installers is not supported — package on the target OS (CI matrix).
- Customize the build by editing the Taskfile tasks (
build/Taskfile.*.yml), not by inventing flags.
wails3 doctor diagnoses missing platform dependencies (WebView2 on Windows, webkit2gtk on Linux).
Dialogs, menus, tray
application.InfoDialog().SetTitle("Done").SetMessage("Export complete").Show()
path, _ := application.OpenFileDialog().AddFilter("Images", "*.png;*.jpg").PromptForSingleSelection()
menu := application.NewMenu()
file := menu.AddSubmenu("File")
file.Add("Open…").SetAccelerator("CmdOrCtrl+O").OnClick(func(ctx *application.Context) { … })
app.Menu.SetApplicationMenu(menu)
tray := app.SystemTray.New()
tray.SetMenu(trayMenu)
Builder-style APIs throughout; dialogs are safe to call from service methods (they marshal to the main thread internally). Exact constructor names vary across alphas — verify against the installed version.
Pitfalls
- Applying v2 docs/snippets to v3 — the top failure mode. Red flags in generated code:
runtime.* imports, ctx context.Context first params on bound methods, wails.Run(&options.App{...}), EventsOn from @wailsapp/runtime (v3 uses @wailsio/runtime).
- Blocking the main thread — long work inside
InvokeSync, event callbacks on the main thread, or app hooks freezes all windows. Push work to goroutines and report via events.
- Forgetting to regenerate bindings after changing Go signatures — frontend keeps calling the old shape and fails at runtime.
- Unguarded shared state in services — bound calls are concurrent; use mutexes or channels.
- Doing init in
main instead of ServiceStartup — services created before app.Run() can't touch windows/events yet; lifecycle hooks run at the right time.
- Assuming API stability — pin the v3 alpha version in
go.mod and match @wailsio/runtime to it; upgrade both together.