Skip to content

Native capability reference 📇

This page catalogs every capability group in the tempestweb.native bridge — one per section, with a motivating sentence and a complete, runnable snippet. It is the pocket reference for Track T (web-platform parity); for the didactic introduction ("one API, three paths"), start with Capabilities.

One import for everything

Every example below starts with the same line:

from tempestweb import native

From there you call native.<group>.<verb>(...). The signature is the same in Mode A (WASM), Mode B (server), and Mode C (transpile) — the --mode only chooses how the call reaches the Web API.

await vs async for — two shapes

There are two capability shapes, and you can tell which from how you consume it:

One request, one result. The vast majority of capabilities. You await and get the typed value back.

from tempestweb import native

online = await native.network.state()   # → NetworkState

One subscription, many events over time. Consumed with async for; exiting the loop (end, break, cancellation) closes the subscription automatically.

from tempestweb import native

async for pos in native.geolocation.watch():   # T-EV
    app.set_state(lambda s: setattr(s, "here", pos))

Streaming capabilities run over the native event channel (T-EV) — see the event-channel tutorial.

In Mode B the call has a deadline

In Mode A the capability runs in the same process. In Mode B it is proxied: the server sends a native_call and waits for the browser's native_result. A tab closed mid-call, or a capability that broke before replying, would leave that await suspended forever — so it fails with NativeError("timeout") after DEFAULT_NATIVE_CALL_TIMEOUT (30s), which is ample for a permission prompt or a file picker. Treat timeout like any other error code; to change the deadline, build the session with a ProxyBridge(send_frame, timeout=...).

Secure context and Chromium-only

Many capabilities require HTTPS (or localhost) and some exist only in Chromium (Chrome/Edge). Each risky group exposes an is_supported() so you can degrade gracefully — treat "unsupported" as a normal flow, never a crash.


Tier 1 — universal, cheap, high value

Widely supported across all modern browsers. These are the backbone of PWA parity.

vibration — buzz the device

Give tactile feedback with a single burst or an on/off pattern.

from tempestweb import native

async def on_success() -> None:
    await native.vibration.vibrate([100, 50, 100])   # ms: buzz, pause, buzz

badge — count on the PWA icon

Mark the installed app's icon with an unread count (or a generic dot).

from tempestweb import native

async def sync_badge(unread: int) -> None:
    if unread:
        await native.badge.set_badge(unread)   # 0 or None also clears
    else:
        await native.badge.clear()

wakelock — keep the screen awake

Prevent the screen from sleeping during a read, recipe, or video. Keep the id that request() returns to release it later.

from tempestweb import native

async def start_reading() -> str:
    lock_id = await native.wakelock.request()
    return lock_id

async def stop_reading(lock_id: str) -> None:
    await native.wakelock.release(lock_id)

fullscreen — fullscreen mode

Enter and exit fullscreen; read the current state. Each call returns whether fullscreen is active afterward.

from tempestweb import native

async def toggle_fullscreen() -> bool:
    if await native.fullscreen.state():
        await native.fullscreen.exit()
        return False
    return await native.fullscreen.enter()

network — connection conditions

Read (state) or watch (watch, streaming) onLine, effectiveType, downlink, rtt, and saveData — perfect for adapting the UI to slow networks.

from tempestweb import native

async def read_network() -> None:
    net = await native.network.state()   # → NetworkState
    print(net.online, net.effective_type, net.save_data)

async def follow_network() -> None:
    async for net in native.network.watch():   # streaming (T-EV)
        app.set_state(lambda s: setattr(s, "online", net.online))

imaging — compress, thumbnail and transform before upload

Between camera.capture() and http.upload() there was nothing: an app captured a 4 MB photo and uploaded 4 MB, or rewrote canvas compression by hand.

from tempestweb import native

async def upload_photo() -> None:
    photo = await native.camera.capture(include_bytes=False)
    small = await native.imaging.compress(photo, max_kb=200, max_width=1600)
    print(small.size_kb, small.quality, small.attempts, small.within_budget)
    await native.http.upload("/api/photos", small.as_upload("photo.jpg"))

The pixels stay in the browser

Every function here takes and returns an opaque handle to bytes the client is holding. Python addresses the image by name; the image never crosses the bridge:

Mode B, a 4 MB photo, compressing it:

  bytes:  client →5.3MB→ server →5.3MB→ client   (10.6 MB of network)
  handle: client →"blob:tw:7"→ server →"blob:tw:7"→ client   (~40 bytes)

camera.capture(include_bytes=False) extends that to the first crossing, and small.as_upload(name) to the last — the server gets the bytes, Python never does.

Measured on Chrome 150, a 4000×3000 structured photo (gradient + shapes):

original 871.5 KB
after compress(max_kb=200, max_width=1600) 124.8 KB (−85.7%)
quality chosen 0.91
encodes spent 5
within_budget True
time 545 ms
thumbnails 96 / 256 px 4.9 KB / 29.7 KB

An impossible budget answers; it does not hang

The quality search is binary and bounded by steps (6 by default). Measured with 9.4 MB of pure noise against a 200 KB budget: it stopped after 5 encodes with within_budget=False and size_kb=573.9 — the smallest it managed. A too-large image the app can decide about beats an endless spinner.

Check within_budget.

Four capabilities, plus two for housekeeping:

Call Answers
compress(source, max_kb=, max_width=, ...) CompressedImage(ref, size_kb, quality, attempts, within_budget, …)
thumbnails(source, [96, 256]) list[Thumbnail], in the order asked
transform(source, width=, rotate=, crop=, flip_horizontal=) ProcessedImage — all in one pass
info(source) ImageInfo(mime_type, width, height, size_kb), without re-encoding
read(source) ImageBytes — the escape hatch, moves the whole image
release(source) / release(all=True) frees a handle

A misspelled option raises

CompressOptions and TransformOptions are extra="forbid": compress(photo, maxWidth=1600) raises instead of being silently ignored — the silence would upload the photo at full size and nobody would know. CompressedImage, Thumbnail and friends ignore unknown fields, or a newer client would break an older Python.

Handles are bounded, and an expired one is a named error

The client holds a few dozen blobs and drops the oldest, so a capture screen running for an hour does not accumulate every frame. Addressing an expired handle raises NativeError("not_found") — recover by capturing again, not by retrying.

device — memory, cores and heap, for adaptive quality

Describes the user's machine coarsely, so an app can decide whether to compress the photo harder, cache less, or give up on running the ONNX model locally.

from tempestweb import native

async def choose_quality() -> int:
    profile = await native.device.profile()   # → DeviceProfile(memory_gb, cores, heap_used_mb, heap_limit_mb)
    if profile.memory_gb is not None and profile.memory_gb <= 2:
        return 60
    network = await native.network.state()
    if network.save_data or network.effective_type in {"slow-2g", "2g", "3g"}:
        return 70
    return 85

Every field is optional, and None does not mean "weak"

navigator.deviceMemory and performance.memory are Chromium-only. On Safari and Firefox the call succeeds and answers None for most of it. An app reading None as "weak device" degrades every iPhone to its worst quality tier — the opposite of what adapting was for. Branch on a known value and let the unknown fall through to your default.

Only hardware lives here

Connection type is network and storage usage is quota. Repeating either here would give one fact two names in the contract, and the two names would drift.

This is for adapting quality, not for identifying anyone

The fields are coarse on purpose. Do not send this anywhere as an identifier.

Measured on Chrome 150: memory_gb=32, cores=12, heap_used_mb=2.5, heap_limit_mb=4192. memory_gb is quantized to a power of two and the browser may cap it — compare with <= against a low threshold, not against an exact value.

visibility — tab focused or hidden

Know whether the page is "visible" or "hidden" — pause animations/polling when the user switches tabs.

from tempestweb import native

async def pause_when_hidden() -> None:
    async for vis in native.visibility.watch():   # streaming (T-EV)
        app.set_state(lambda s: setattr(s, "playing", vis == "visible"))

orientation — screen orientation

Lock/unlock the orientation and read the current type/angle; watch rotations as a stream.

from tempestweb import native

async def lock_landscape() -> bool:
    return await native.orientation.lock("landscape")   # requires fullscreen

async def follow_rotation() -> None:
    async for o in native.orientation.watch():   # streaming (T-EV)
        app.set_state(lambda s: setattr(s, "angle", o.angle))

quota — storage usage and persistence

Estimate the origin's usage/quota and ask for persistent storage (exempt from eviction under pressure). Pairs with storage/offline.

from tempestweb import native

async def ensure_durable() -> None:
    est = await native.quota.estimate()   # → StorageEstimate(usage, quota)
    if not await native.quota.persisted():
        await native.quota.persist()

clipboard (image) — copy/paste images

Beyond read/write for text, it now reads and writes images (base64 + MIME).

from tempestweb import native

async def paste_image() -> None:
    img = await native.clipboard.read_image()   # → ClipboardImage
    app.set_state(lambda s: setattr(s, "png_b64", img.data_base64))

async def copy_image(png_b64: str) -> None:
    await native.clipboard.write_image(png_b64, mime_type="image/png")

battery — level and charge (streaming)

Watch level, charging state, and estimated times. Streaming only — every change emits a fresh BatteryStatus.

from tempestweb import native

async def follow_battery() -> None:
    async for b in native.battery.watch():   # streaming (T-EV)
        app.set_state(lambda s: setattr(s, "battery", b.level))

sensors — orientation and motion (streaming)

Continuous accelerometer/gyroscope readings via Device Orientation / Motion.

from tempestweb import native

async def follow_tilt() -> None:
    async for o in native.sensors.orientation():   # streaming (T-EV)
        app.set_state(lambda s: setattr(s, "beta", o.beta))

async def follow_motion() -> None:
    async for m in native.sensors.motion():   # streaming (T-EV)
        app.set_state(lambda s: setattr(s, "accel", m.acceleration))

Permission on iOS

On Safari iOS, deviceorientation/devicemotion require explicit permission granted from a user gesture. The subscription raises NativeError (permission_denied) when refused — treat it as a normal flow.


Tier 2 — widely used

Well supported in most browsers; some ask for permission.

speech — synthesis (TTS) and recognition (STT)

Speak text aloud (single-shot) and list voices; recognize speech as a stream.

from tempestweb import native

async def announce(text: str) -> None:
    await native.speech.speak(text, lang="en-US", rate=1.0)

async def dictate() -> None:
    async for r in native.speech.listen(lang="en-US"):   # streaming (T-EV)
        if r.is_final:
            app.set_state(lambda s: setattr(s, "said", r.transcript))

recorder — record audio, video, or the screen

Start recording from the microphone or the screen; stop returns the bytes as base64.

from tempestweb import native

async def record_clip() -> None:
    rec_id = await native.recorder.start(source="microphone")
    # … user speaks …
    recording = await native.recorder.stop(rec_id)   # → Recording
    app.set_state(lambda s: setattr(s, "clip_b64", recording.data_base64))

filesystem — read and write files with live handles

Open files through the system picker (with a reusable handle to write back), or create a new file with the save picker.

from tempestweb import native

async def open_and_edit() -> None:
    files = await native.filesystem.open_file(accept=".txt", multiple=False)
    if files:
        handle = files[0]                       # → FileHandle
        await native.filesystem.write_file(handle.id, handle.data_base64)

async def save_new(data_b64: str) -> None:
    await native.filesystem.save_file("export.bin", data_b64)

bgsync — Background Sync + Periodic Sync

Register work the service worker replays when connectivity returns (or on a periodic interval) — the engine behind the real offline-queue replay.

from tempestweb import native

async def queue_sync() -> None:
    await native.bgsync.register("outbox")
    await native.bgsync.register_periodic("refresh", min_interval_ms=3_600_000)

tabs — sync across tabs

Broadcast messages between tabs (BroadcastChannel) and coordinate with named locks (Web Locks). Receiving messages is streaming.

from tempestweb import native

async def broadcast_theme(theme: str) -> None:
    await native.tabs.broadcast("prefs", {"theme": theme})

async def follow_prefs() -> None:
    async for msg in native.tabs.receive("prefs"):   # streaming (T-EV)
        app.set_state(lambda s: setattr(s, "theme", msg["theme"]))

idle — idle detection (streaming)

Know when the user goes idle or the screen locks.

from tempestweb import native

async def follow_idle() -> None:
    async for state in native.idle.watch(threshold_seconds=120):   # streaming (T-EV)
        app.set_state(lambda s: setattr(s, "away", state.user == "idle"))

Tier 3 — niche, secure-context, mostly Chromium-only

Powerful but narrowly supported. Always check is_supported() first and keep a fallback.

Chromium-only + secure context

The groups below (with rare exceptions) exist only in Chromium (Chrome/Edge), require HTTPS, and most open a system picker that needs a user gesture. Firefox/Safari usually return is_supported() == False.

bluetooth — Web Bluetooth (GATT)

Pair a BLE device and read/write GATT characteristics.

from tempestweb import native

async def read_heart_rate() -> str:
    if not await native.bluetooth.is_supported():
        return ""
    device = await native.bluetooth.request(
        optional_services=["heart_rate"],
    )                                            # → BluetoothDevice
    return await native.bluetooth.read(device.id, "heart_rate", "heart_rate_measurement")

usb — WebUSB

Request access to a USB device through the browser chooser.

from tempestweb import native

async def pick_usb() -> None:
    if await native.usb.is_supported():
        device = await native.usb.request(filters=[{"vendorId": 0x2341}])
        print(device.product_name, device.vendor_id)

serial — Web Serial

Open a serial port (Arduino, readers, etc.); returns an opaque port id.

from tempestweb import native

async def pick_serial() -> str:
    if not await native.serial.is_supported():
        return ""
    return await native.serial.request(filters=[])

hid — WebHID

Request access to HID devices (exotic gamepads, special keyboards).

from tempestweb import native

async def pick_hid() -> list[dict[str, object]]:
    if not await native.hid.is_supported():
        return []
    return await native.hid.request(filters=[])

nfc — Web NFC (write)

Write NDEF records to a nearby NFC tag.

from tempestweb import native

async def write_tag(url: str) -> None:
    if await native.nfc.is_supported():
        await native.nfc.write([{"recordType": "url", "data": url}])

NFC read (scan) — streaming (T-EV)

Beyond writing, tag scanning is a continuous stream over the event channel:

async for msg in native.nfc.scan():
    print(msg.serial_number, msg.records)

Each NdefMessage carries serial_number + decoded records; exiting the loop aborts the scan. See Native event channel.

contacts — Contact Picker

Let the user pick contacts through the system picker (Android/Chrome).

from tempestweb import native

async def pick_contact() -> list[dict[str, object]]:
    if not await native.contacts.is_supported():
        return []
    return await native.contacts.select(properties=["name", "tel"], multiple=False)

payment — Payment Request API

Show the browser's native payment sheet.

from tempestweb import native

async def checkout() -> dict[str, object]:
    if not await native.payment.is_supported():
        return {}
    return await native.payment.request(
        methods=[{"supportedMethods": "https://example.com/pay"}],
        details={"total": {"label": "Total", "amount": {"currency": "USD", "value": "9.90"}}},
    )

pip — Picture-in-Picture

Pop a <video> into a floating window.

from tempestweb import native

async def pop_video() -> bool:
    return await native.pip.request(selector="video#player")

async def close_pip() -> None:
    await native.pip.exit()

eyedropper — color eyedropper

Let the user pick a color from anywhere on screen.

from tempestweb import native

async def pick_color() -> str:
    return await native.eyedropper.open()   # → "#3366ff" (or "" if cancelled)

pointerlock — lock the pointer

Capture the mouse (games, 3D viewers), hiding the cursor.

from tempestweb import native

async def start_game() -> None:
    await native.pointerlock.request(selector="#canvas")

async def end_game() -> None:
    await native.pointerlock.exit()

gamepad — Gamepad API

Read a snapshot (state) or watch the controls as a stream (watch).

from tempestweb import native

async def read_pads() -> list[dict[str, object]]:
    return await native.gamepad.state()

async def follow_pads() -> None:
    async for pads in native.gamepad.watch():   # streaming (T-EV)
        app.set_state(lambda s: setattr(s, "pads", pads))

midi — Web MIDI

Enumerate ports, send messages, and listen to incoming messages as a stream.

from tempestweb import native

async def play_note() -> None:
    if not await native.midi.is_supported():
        return
    ports = await native.midi.request_access()   # → MidiPorts
    if ports.outputs:
        await native.midi.send(ports.outputs[0]["id"], [0x90, 60, 0x7F])

async def follow_midi() -> None:
    async for msg in native.midi.messages():   # streaming (T-EV)
        app.set_state(lambda s: s.notes.append(msg.data))

webaudio — tone, phrase and meter

Three shapes, in increasing order of what they can say.

One beep, with no audio asset needed (unlike audio.play):

from tempestweb import native

async def beep() -> None:
    await native.webaudio.tone(frequency=880.0, duration_ms=150, type="sine")

A whole phrase, in one call. Every Step gets its own oscillator and gain on a shared bus, with an attack/release envelope. Steps sharing a start_ms sound together — that is how a chord is written:

from tempestweb import native

async def chord() -> None:
    result = await native.webaudio.sequence(
        [
            native.webaudio.Step(frequency=261.63, duration_ms=700, gain=0.3),
            native.webaudio.Step(frequency=329.63, duration_ms=700, gain=0.3),
            native.webaudio.Step(frequency=392.00, duration_ms=700, gain=0.3),
        ]
    )
    print(result.scheduled, result.ends_in_ms, result.blocked)   # 3 700 False

async def hush() -> None:
    await native.webaudio.stop()      # cuts what still sounds; the context stays open

Why a phrase, and not a node graph

In Mode B every capability call is a round-trip. An API shaped like Web Audio's own node graph would put the network between an oscillator and its gain. What an app needs from "beyond a single tone" is scheduling and shaping, and both are per-phrase — so the phrase is the unit that crosses the wire.

The envelope is what separates a note from a click

attack_ms/release_ms (5/40 by default) ramp from silence to gain and back. Without them the waveform starts and stops mid-cycle, and what you hear is a click at both edges.

A meter, streaming, over the synthesis itself or the microphone:

from tempestweb import native

async def vu() -> None:
    async for level in native.webaudio.watch_levels(interval_ms=100, bands=8):
        app.set_state(lambda s: setattr(s, "vu", level.rms))

source="output" (the default) taps the shared bus: no microphone and no permission prompt, so an app can meter the audio it is playing itself. source="mic" opens getUserMedia({audio: true}) and fails with permission_denied if the user refuses.

Verified in Modes A and B

sequence, stop and watch_levels measured in a real Chrome in both interactive modes. In Mode A, on a virgin origin: the chord reports 3 notas juntas, 700 ms, the meter reads rms 0.374 · peak 0.766 while it sounds, and stop returns parado: 2 osciladores.

Mode C reaches no streaming capability

Not about audio: the compiler does not know async for yet (statement AsyncFor is not supported), so no streaming capability is reachable from a Mode C app — the same holds for geolocation.watch. The Mode C facade already exposes watch_levels for hand-written JS, and sequence/stop compile fine.

Long consumption goes through spawn

Both modes read events in series. An async for awaited straight inside a handler holds the dispatch and the app stops responding — hand it to tempestweb.runtime.spawn, the way examples/webaudio_demo does. That, in an old artifact served by the service worker of a reused origin, is exactly what produced the false positive in #171.

Measured in a real Chrome

With a 700 ms chord sounding: rms 0.365 → 0.376 → 0.353, peak 0.852 → 0.719; at t=720 ms — when the release ends — back to 0.000. A 4-note staggered arpeggio climbs rms 0.184 → 0.286 → 0.342 as the notes overlap. examples/webaudio_demo is that app.


Recap

  • One import (from tempestweb import native) and the same signature across the three modes.
  • Two shapes: single-shot with await, streaming with async for (over the T-EV event channel).
  • Tier 1 is universal; Tier 2 is widely used; Tier 3 is Chromium-only/ secure-context and always ships is_supported() + a fallback.
  • Streams (geolocation.watch, sensors.*, network.watch, visibility.watch, orientation.watch, battery.watch, speech.listen, idle.watch, tabs.receive, gamepad.watch, midi.messages, nfc.scan) close the subscription when the loop exits.
  • Track T is complete, with no known capability gaps.

See the bridge in action in the Device panel and the call wire format in docs/contract.md. 🚀