MOBIGO

DOCS

v0.1 ALPHAAndroid only for now. APIs may change before 1.0.

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:

SHELL
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

SHELL
$ 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

PathWhat
mobigo.jsonName, app id, head files, permissions
app/app.goYour 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

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:

GO
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
}
HelperDoes
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, …) or mobigo.Trigger(w, "mobigo:name", value)). mobigo.js routes mobigo:* 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:

app_test.go
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:

  1. Generates gen/bind (the gomobile package) and gen/dev.
  2. Creates native/, a Capacitor Android project, on first build, then rewrites the MoBiGo plugin and activity.
  3. Compiles your Go to mobigoapp.aar with gomobile bind.
  4. Writes the shell page, MoBiGo's JS and your static files into native/www.
  5. Runs cap sync and 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.

ONE ROUND TRIP
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.