DOCS
MoBiGo turns a Go web app into a phone app. You write handlers that return HTML; htmx swaps it into the page; the whole thing runs on the device, with no server.
If you've built a Go + htmx site before, you already know almost everything here.
Install
You need Go 1.25+, Node, Java 21 and the Android SDK with an NDK. On macOS:
brew install go node openjdk@21 brew install --cask android-commandlinetools sdkmanager "platform-tools" "platforms;android-36" \ "build-tools;36.0.0" "ndk;28.2.13676358" "emulator" \ "system-images;android-36;google_apis;arm64-v8a" go install mobigo.codenexis.com/cmd/mobigo@latest mobigo doctor
mobigo doctor finds your tools without any environment variables and tells you what's missing. gomobile is installed for you on your first build.
Your first app
$ mobigo new hello $ cd hello $ mobigo dev # http://127.0.0.1:8080, reloads on save $ go test ./... $ mobigo run android # starts an emulator if no phone is plugged in
mobigo dev serves your app the way the phone does: the browser loads a shell page and every htmx request goes through the same bridge into your handler. Add --transport http to serve plain HTTP instead.
Project layout
| Path | What |
|---|---|
mobigo.json | Name, app id, head files, permissions |
app/app.go | Your New function, routes and handlers |
app/templates/ | Your HTML (html/template, or anything else) |
app/static/ | CSS, JS, images, served at /static/ |
gen/ | Generated glue (gitignored) |
native/ | Generated Android project (gitignored) |
mobigo.json
{
"name": "Keeper",
"appId": "dev.mobigo.keeper",
"static": "app/static",
"head": ["app.css", "app.js"],
"permissions": ["camera"]
}head lists files in static to load on every page. permissions currently supports camera.
The API
Your app package exports one function. The runtime calls it once at start-up:
func New(env mobigo.Env) (*mobigo.App, error) type Env struct { DataDir string // private folder that survives restarts Dev bool // true under mobigo dev } type App struct { Handler http.Handler // your routes Static fs.FS // served at /static/ Close func() error // optional, e.g. close your database }
| Helper | Does |
|---|---|
HeadTags(r) | Script and style tags for your page's head |
IsFragment(r) | htmx wants part of a page, not the whole page |
Trigger(w, event, value) | Fire an event on the page (merged into HX-Trigger) |
Toast(w, msg) | Fire mobigo:toast for your UI to show |
Haptic(w, style) | Vibrate: HapticLight, HapticSuccess, HapticError |
FormError(w, target, msg) | 422 with the message swapped into target |
ViaBridge(r) | The request came from inside the phone app |
Your page template puts {{.Head}} in its head (from mobigo.HeadTags(r)). For a fragment request to /, render just the body content: that's what the phone's shell page asks for first.
Native features
Go never calls the camera itself. Native features run as Capacitor plugins, and talk to Go two ways:
- Go to phone: set an event (
mobigo.Haptic(w, …)ormobigo.Trigger(w, "mobigo:name", value)).mobigo.jsroutesmobigo:*events to handlers. - Phone to Go: post the result to a route with
htmx.ajax, like any other request.
Add your own handler in JS: Mobigo.native('share', function (value) { … }). The Keeper example uses the WebView camera (getUserMedia) and posts frames to Go, which decodes the QR codes.
Testing
mobigotest drives your app through the same JSON bridge the phone uses, with htmx headers:
func TestTap(t *testing.T) { phone := mobigotest.New(t, New) // fresh DataDir per test res := phone.Post("/tap", nil) if res.Status != 200 || !res.Triggered("mobigo:haptic") { t.Fatalf("got %d %v", res.Status, res.Headers) } }
Use mobigotest.NewIn(t, dir, New) twice with the same folder to check data survives a restart.
Building for Android
mobigo build android does, in order:
- Generates
gen/bind(the gomobile package) andgen/dev. - Creates
native/, a Capacitor Android project, on first build, then rewrites the MoBiGo plugin and activity. - Compiles your Go to
mobigoapp.aarwithgomobile bind. - Writes the shell page, MoBiGo's JS and your static files into
native/www. - Runs
cap syncand Gradle, giving you a debug APK.
mobigo run android also installs and launches it, starting your first emulator if nothing is connected.
The bridge
On the phone, mobigo.js replaces XMLHttpRequest with a small shim. htmx doesn't know: it builds requests as usual, and the shim sends each one to Go as JSON through the native plugin.
htmx --{"method":"POST","path":"/tap"}--> MobigoPlugin.serve
|
Go: http.Handler.ServeHTTP
|
htmx <--{"status":200,"body":"<...>"}-----------+Bodies travel inside the message, so POSTs work even where web views drop request bodies. A localhost transport (random port, per-launch token) is also available for streaming.