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 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.
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:
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 withasync 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. 🚀