Goleo
Build cross-platform desktop and mobile apps with a Go backend and a web UI.
Write your app logic in Go. Build your interface with any web framework β Vue, React,
Svelte, or plain HTML. Ship one codebase to Windows, macOS, Linux, Android, iOS, and the
web (PWA).
Goleo is in the Tauri / Wails class β a compiled backend driving the OS's native webview β but
the backend is Go, and a single project targets desktop and mobile and PWA.
app.Bridge().Handle("greet", func(ctx context.Context, args json.RawMessage) (any, error) {
var p struct{ Name string `json:"name"` }
_ = json.Unmarshal(args, &p)
return "Hello, " + p.Name + "!", nil
})
import { invoke } from '@goleo/bridge'
const msg = await invoke<string>('greet', { name: 'World' }) // β "Hello, World!"
π Developer Guide
Full step-by-step docs live in docs/guide/ β
installation, project setup, building, packaging (icons + metadata + installers),
deploying & auto-update, wiring up your app, RPC, native menus, system tray, and
mobile (device dev + sideloading).
Why Goleo
- π₯οΈ One codebase, six targets β Windows, macOS, Linux, Android, iOS, PWA.
- πΉ Real Go on the backend β full concurrency, the standard library, and the Go ecosystem
for your app logic and system access.
- π¨ Any web frontend β Vue (default template), React, Svelte, vanilla β anything Vite builds.
- π Typed bridge β call Go from JS with
invoke(), push events from Go with Emit();
generate TypeScript types for every command.
- π¦ Small single binary β the frontend is embedded via
//go:embed; no bundled Chromium
(uses the OS webview).
- π Ships itself β installers, code signing/notarization, and a signed auto-updater
built into the CLI.
- πͺ Native desktop integration β multi-window, system tray, single-instance, launch-on-login,
deep links, and a signal-based lifecycle.
- π Secure by design β an opt-in runtime capability ACL and a hardened loopback bridge.
- π± Device features β clipboard, filesystem, dialogs, share, camera, geolocation, battery,
sensors, notifications, and more β native where it counts, graceful browser fallbacks elsewhere.
Quick start
# Install the CLI (npm, or `go install github.com/daforester/goleo/cli/goleo@latest`)
npm install -g @goleo/cli
# Scaffold a project (Vue + Vite + @goleo/bridge). Prompts for a template;
# add --demo for the full host-feature showcase. `npx @goleo/cli new` works too.
goleo new my-app
cd my-app
cd frontend && npm install && cd ..
# Live development (Go backend + Vite HMR)
goleo dev
# Build a single binary for the current platform
goleo build
Then edit backend/app/app.go (startup + feature wiring), backend/commands/ (your Go
commands), and frontend/src/ (your UI). That's it.
Project layout
my-app/
βββ goleo.json # project + bundle/publish config
βββ backend/
β βββ app/app.go # the one file you edit: config, command + feature registration
β βββ commands/ # your backend commands
β βββ main.go # generated (desktop entry point) β do not edit
β βββ gomobile/ # generated (mobile entry points) β do not edit
βββ frontend/ # your web app (Vite; Vue by default, swappable)
βββ src/main.ts # inits the bridge
main.go and the gomobile/ glue are regenerated on every build β all your logic lives in
backend/app/app.go and backend/commands/.
The bridge β calling Go from your UI
Go β register a command:
app.Bridge().Handle("saveNote", func(ctx context.Context, args json.RawMessage) (any, error) {
var note struct{ Title, Body string }
if err := json.Unmarshal(args, ¬e); err != nil {
return nil, err
}
// ...persist it...
return map[string]string{"id": "note-1"}, nil
})
// Push an event to the frontend at any time:
app.Emit("sync:done", map[string]any{"count": 42})
TypeScript β invoke it and listen for events:
import { invoke, on } from '@goleo/bridge'
const { id } = await invoke<{ id: string }>('saveNote', { title: 'Hi', body: '...' })
const unsubscribe = on('sync:done', (data) => console.log('synced', data))
Run goleo generate types to produce frontend/src/goleo.d.ts with a fully typed invoke()
for every built-in command.
Desktop capabilities
Configure in runtime.Config and call the bridge helpers from @goleo/bridge.
Multi-window, tray, and lifecycle:
a = runtime.New(runtime.Config{
Title: "My App",
InProcessWindows: true, // extra windows in-process (Windows: own message loop;
// macOS/Linux: share the primary's run loop)
SingleInstance: true, // a second launch focuses the running one
URLScheme: "myapp", // register myapp:// deep links
Background: true, // headless controller (window(s) on demand)
Tray: &runtime.TrayConfig{ // optional system tray
Tooltip: "My App",
Items: []runtime.TrayItem{
{Label: "Open", OnClick: func() { a.OpenWindow(runtime.WindowOptions{Path: "/"}) }},
{Label: "Quit", OnClick: func() { a.Quit() }},
},
},
})
import { openWindow, quitApp, getInitialURL, onDeepLink,
enableAutostart, checkForUpdate, applyUpdate } from '@goleo/bridge'
await openWindow({ path: '/settings', width: 600, height: 400 })
onDeepLink((url) => route(url)) // myapp:// links while running
await enableAutostart() // launch on login
if ((await checkForUpdate()).available) await applyUpdate()
| Capability |
Config / API |
| Additional windows |
App.OpenWindow Β· openWindow/closeWindow/listWindows Β· WindowOptions.ExitOnClose |
| System tray |
Config.Tray + Config.Background |
| Single instance |
Config.SingleInstance β app:secondInstance event |
| Launch on login |
enableAutostart / disableAutostart / isAutostartEnabled |
| Deep links |
Config.URLScheme β getInitialURL + onDeepLink |
| Graceful quit |
App.Quit() Β· quitApp() |
| Auto-update |
runtime.RegisterUpdater + checkForUpdate/applyUpdate |
Device features & storage
Register the ones you use in backend/app/app.go; the CLI auto-detects them for mobile builds.
Each has a TS wrapper with a browser fallback, so PWA/dev degrade gracefully.
runtime.RegisterDesktopFeatures(a.Bridge()) // clipboard, dialogs, filesystem
runtime.RegisterStore(a.Bridge()) // persistent key/value store
runtime.RegisterShare(a.Bridge()) // native share sheet
runtime.RegisterClipboard(a.Bridge())
runtime.RegisterCamera(a.Bridge()) // + geolocation, battery, sensors, vibration, nfc, bleβ¦
import { storeSet, storeGet, share, clipboardReadText } from '@goleo/bridge'
await storeSet('theme', 'dark')
const theme = await storeGet<string>('theme')
await share({ title: 'Goleo', url: 'https://example.com' })
Available: clipboard Β· dialogs Β· filesystem Β· key/value store Β· share Β· camera Β·
geolocation Β· battery Β· sensors Β· vibration Β· wake-lock Β· NFC Β· Bluetooth (BLE) Β· notifications Β·
background Β· push.
Security β capability ACL
Bridge access is permissive by default; set a Policy to switch to deny-by-default on a
per-method basis (Tauri-style, enforced centrally on every invoke):
app.SetPolicy(&runtime.Policy{
Allow: []string{"goleo:store*", "greet"}, // exact or "prefix*"; core info commands always allowed
FSRoots: []string{"/home/me/app-data"}, // widen the filesystem scope
})
Filesystem access is confined by default. The fs plugin can reach the app's own data
directory, anything in Policy.FSRoots, and any path the user picked in a native file dialog
this session β nothing else. Writes and deletes outside that scope are refused; out-of-scope
reads currently log a deprecation warning and will become errors. Set
Config.FSScope = runtime.FSScopeUnrestricted (or GOLEO_FS_UNRESTRICTED=1) for a tool that
genuinely needs the whole disk. System locations are refused for writes in every mode.
HTTPHosts and ShellPrograms are reserved β goleo has no http or shell plugin yet, so
they gate nothing today.
The loopback bridge is also hardened in production (loopback-only bind, origin allow-list,
per-launch token). goleo:openURL only opens http/https/mailto/tel plus your own
Config.URLScheme.
Distribution & auto-update
The CLI takes you from build to a self-updating installer:
goleo build --bundle # native installer: NSIS .exe Β· .dmg Β· .deb/.rpm
goleo build --bundle --publish # + write an ed25519-signed update manifest
goleo generate updater-key # keypair for signing updates
Code signing/notarization are env-driven (GOLEO_WIN_CERT, GOLEO_MAC_IDENTITY,
GOLEO_APPLE_ID, β¦) so secrets stay out of the repo and CI can inject them. The in-app updater
verifies the signed manifest before applying an update.
CLI reference
| Command |
What it does |
goleo new <name> |
Scaffold a new project |
goleo dev |
Dev mode β Go backend + Vite HMR |
goleo dev pwa |
Frontend-only PWA dev (no Go backend) |
goleo build [target] |
Build for current/windows/linux/darwin/android/ios/pwa |
goleo build --bundle |
Also produce a native installer |
goleo build --publish |
Also write the signed update manifest |
goleo emulate android |
Build + run on a connected Android emulator/device |
goleo generate types |
Generate goleo.d.ts typed bridge bindings |
goleo generate updater-key |
Generate an ed25519 update-signing keypair |
|
Desktop app |
Mobile app |
PWA |
Native webview |
| Windows |
β
|
β |
β
|
WebView2 (cgo-free) |
| macOS |
β
|
β |
β
|
WKWebView (cgo-free) |
| Linux |
β
|
β |
β
|
WebKitGTK (cgo-free) |
| Android |
β |
β
|
β
|
system WebView (gomobile) |
| iOS |
β |
β
|
β
|
WKWebView (gomobile) |
All three desktop builds are fully cgo-free and cross-compile from one machine β the glaze
binding (WKWebView / WebKitGTK / WebView2 via purego) drives the OS webview with no C toolchain in
the loop. Mobile builds use gomobile; iOS requires macOS + Xcode.
How it works
ββββββββββββββββββββββββββ invoke / events ββββββββββββββββββββββββββ
β Web UI (OS webview) β ββββββββββββββββββββΊ β Go backend + Bridge β
β @goleo/bridge β WebSocket Β· HTTP β your commands + feats β
ββββββββββββββββββββββββββ Β· native bind ββββββββββββββββββββββββββ
- Dev: Vite serves the UI with HMR and proxies to the Go backend.
- Production: the frontend is embedded in the Go binary (
//go:embed) and served from a
hardened loopback server; the OS webview loads it.
- Mobile: a
gomobile .aar/.xcframework (an intermediate; Gradle turns the AAR into the app.apk/app.aab you ship) runs the same Go backend inside the platform's
native WebView.
Deeper docs: AGENTS.md (architecture), docs/roadmap.md
(masterplan + status), SPIKES.md (feasibility findings),
docs/comparison.md (vs Tauri v2 & Wails v3).
Status
Feature-complete and shipping-ready on all targets via the paths above. The desktop webview is now
cgo-free on all three OSes (macOS / Linux / Windows via glaze); see the roadmap for what's
next.
License
MIT