QORMQORM v0.8.4 docs Get started

User middle layer: adding your own capabilities to an app

QORM ships with 43 built-in hardware capabilities (camera/recording, location, Bluetooth, NFC, biometrics, vibration/haptics, flashlight, brightness/volume, battery, sensors, network status, clipboard, sharing, notifications, screenshot/screen recording, and more), and app developers can add their own capabilities without modifying framework source code. Go is recommended -- write it once, run it everywhere. Swift/Java are reserved only for rare pure-native SDKs.

Write a native/desktop.go and register your op with qormext.Register. It is compiled into the desktop binary and also compiled into the offline WASM for mobile/web -- the same Go source for iOS/Android/desktop/browser.

//go:build ignore

package main
import "github.com/qorm/qorm/pkg/qormext"
func init() {
    qormext.Register("myBankSDK", func(data map[string]any) string {
        // Your Go logic: algorithms, protocols, HTTP, backend integration... data is the qormToNative payload
        return `qormOnBankSDK("done")`  // return one line of JS, executed back in the app
    })
}

In a component, qormToNative('myBankSDK', {...}) is handed first to this Go (window.qormWasmOp in a WebView, the compiled-in binary on desktop), and the qormOnBankSDK callback updates the UI.

The Go middle layer calls hardware / framework internals directly

From Go you can reach framework internals directly, rather than relying only on returning JS:

qormext.Native("bluetoothScan", `{}`)     // → framework native bridge or Web API
qormext.Emit("orderDone", `{"id":42}`)     // → push an event to the UI event bus; the frontend receives it via qormOn('orderDone', fn)
qormext.CallJS(`navigator.vibrate(200)`)   // → any JS
When hardware access must go through the native bridge, the offline package must include the framework native bridge (see the boundaries at the end).

Bridging contract (built-in hardware + custom, same mechanism)

component/JS  ──①qormToNative(op, data)──►  Go middle layer / framework native bridge
component/JS  ◄──③qormOn<X>(result)────────┘  callback returns to web

qormHasNative() (a native bridge is present) / qormHasMobileNative() (the full iOS/Android bridge); browser/desktop automatically fall back to the Web API.

Advanced: rare pure-native SDKs (Swift / Java injection)

If a capability must use a platform-native API and neither the Web API nor the framework bridge can reach it (certain vendor-proprietary SDKs), add native/ios.swift / native/android.java snippets, which are injected into the generated project at package time. Most of the time this is unnecessary -- use the Go above first.

myapp/native/
    desktop.go      # recommended: Go middle layer (desktop binary + mobile/web WASM)
    web.js          # web side: qormOn<X> callbacks + wiring buttons to ops
    ios.swift       # advanced: rare iOS pure-native SDK
    android.java    # advanced: rare Android pure-native SDK

native/ios.swift

Define a qormUserOp function and dispatch your ops with a switch. The default branch of the iOS bridge's switch calls it. Inside the class you can use the js(_:) callback and body to get the data passed by qormToNative.

func qormUserOp(_ op: String, _ body: [String: Any]) {
    switch op {
    case "myBankSDK":
        let amount = body["amount"] as? Double ?? 0
        // call your real native SDK here...
        js("qormOnBankSDK(\"paid \\(amount) via native SDK\")")
    default:
        break
    }
}

native/android.java

Each op is a @JavascriptInterface method (injected into the Bridge class, exposed on window.qormAndroid). Use the js(String) callback.

@JavascriptInterface public void myBankSDK() {
    runOnUiThread(() -> js("qormOnBankSDK(\"paid via native SDK (Android)\")"));
}

native/web.js

Injected into the page, responsible for two things on the web side: defining qormOn<X> callbacks, and wiring the app's buttons (by id) to qormToNative. This way browser/desktop use the Web API and mobile uses the native bridge, with the same logic.

// click #payBtn → trigger the custom native op
document.addEventListener('click', function (e) {
  if (e.target.closest('#payBtn')) qormToNative('myBankSDK', { amount: 9.99 });
});
// native/Web callback: update the UI
function qormOnBankSDK(msg) {
  var el = document.getElementById('result');
  if (el) el.textContent = msg;
}
A component's id (declared in qorm.json) renders as the DOM id, so web.js can locate it with getElementById / closest('#id').

Complete examples

hash (real crypto/sha256, logic that declarative JSON cannot express), visit (stateful counting held in Go memory), celebrate (Go calling the framework hardware bridge qormext.Native + using qormext.Emit to push events to the UI event bus). A single native/desktop.go, compiled into the desktop binary and the mobile/web WASM alike.

with ios.swift / android.java pure-native escape-hatch snippets.

qorm run examples/middleware                         # desktop: Go middle layer compiled into the binary, runs directly
qorm package examples/middleware -p web              # one Go source compiled into the offline WASM
qorm package examples/native-ext -p ios --dev URL    # inject ios.swift into the dev client

Boundaries and tips

```go //go:build ignore

package main import "github.com/qorm/qorm/pkg/qormext" func init() { qormext.Register("myBankSDK", func(data map[string]any) string { // Your Go logic: HTTP, computation, protocols, backend integration... data is the qormToNative payload return qormOnBankSDK("paid via Go middle-layer") // return one line of JS, which desktop evals back into the page }) } ```

qorm package -p mac runs go build on it together with the framework into a single binary (the packager injects it into cmd/qorm for compilation, then removes it afterward); when the desktop bridge hits an unknown op, it looks it up in the qormext registry. web.js also works as usual (on desktop, camera/microphone/location use the Web API directly, since localhost is a secure context).

//go:build ignore keeps go build ./... from compiling it standalone; at package time the packager strips this line before compiling it in.

Plugin ABI version

The qormext contract (the Op signature, Register, Emit, the bridge) has a version — qormext.ABIVersion. Declare the ABI your native code targets in qorm.json:

{ "pluginABI": "1" }

The loader compares its major to the runtime's ABIVersion and emits a diagnostic if they differ, so an app built against an incompatible middle-layer contract is caught at load time instead of silently misbehaving. It is a warning, not a hard failure — the app still loads; only its custom native ops may not work. Apps that use no versioned middle-layer omit pluginABI entirely (always compatible).

Related: Mobile · Desktop

Custom canvas widgets (native renderer)

The same middle-layer file (native/desktop.go) can also register custom widgets into the native canvas engine — the drawing-side counterpart of custom native ops. The engine ships a widget registry (canvas.RegisterWidget); the built-in library lives outside the engine in internal/widgets/, and your own types register exactly the same way:

//go:build ignore

package main

import (
    "github.com/qorm/qorm/internal/model"
    "github.com/qorm/qorm/internal/render/canvas"
    "github.com/qorm/qorm/internal/render/draw"
    "github.com/qorm/qorm/internal/runtime"
)

func init() { canvas.RegisterWidget("rating", ratingWidget{}) }

type ratingWidget struct{}

func (ratingWidget) Measure(n *model.Node, rt *runtime.Runtime, scale int) (w, h int) {
    return 5 * 24 * scale, 24 * scale
}

func (ratingWidget) Record(ln *canvas.LayoutNode, rt *runtime.Runtime, scale int) draw.Node {
    // compose draw-layer shapes (Rect/Text/Image/Circle/Group) — never touch
    // engine internals. Style parsing, conditional render, disabled and
    // onPress come free from the engine.
}

Scenes then use the type directly: {"type": "rating", "value": 4}, with {{state.x}} bindings evaluated per frame like any other prop. Optional extensions: canvas.InteractiveWidget (pointer/drag, see internal/widgets/slider.go) and canvas.AnimatedWidget (continuous animation, see internal/widgets/spinner.go). The full working app is examples/customwidget.

Note: qorm package compiles the middle layer into the binary (so the widget ships in the app); a plain qorm run of a framework checkout does not inject it, so unregistered custom types render as unknown (empty) nodes.