USER DEVICE & Syncing
USER DEVICE halts a script exactly like GATHER and CONFIRM. It shows a single, fixed popup — one custom label, one Accept button. Accepting does not resubmit immediately: it triggers the browser's real permission prompts for camera, microphone, geolocation, push, and clipboard access, one after another. Only once every prompt has been answered — granted or denied — does the script resume, receiving both the permission results and the plain, permissionless device properties together.
USER DEVICE
The bare form requests everything at once — every permission, the screenshot, and every permissionless property.
USER DEVICE LABEL "We need your permission to access your browser" SET ?device
Requesting a Single Component
Any individual permission can be requested on its own, without asking for anything else. Every single-component request still includes all the free, permissionless properties, since those require no consent and no extra prompt.
USER DEVICE CAMERA LABEL "We need camera access" SET ?device
USER DEVICE MICROPHONE LABEL "We need microphone access" SET ?device
USER DEVICE GEOLOCATION LABEL "We need your location" SET ?device
USER DEVICE PUSH LABEL "Enable notifications" SET ?device
USER DEVICE CLIPBOARD_READ LABEL "Allow reading your clipboard" SET ?device
USER DEVICE CLIPBOARD_WRITE LABEL "Allow writing to your clipboard" SET ?device
USER DEVICE SCREENSHOT
Unlike every other permission field, granting screenshot access immediately produces content, not just a status. The browser shows its own native picker — the visitor chooses to share their entire screen, a single window, or a browser tab — and once granted, the actual captured frame is returned directly. ?device("screenshot") holds the captured image if granted, or null if denied or cancelled.
USER DEVICE SCREENSHOT LABEL "Share your screen to continue" SET ?device
AFTER EMIT ?device("screenshot")
USER DEVICE PERMISSIONLESS
Requests only the properties that need no consent at all — no popup, no LABEL, nothing shown to the visitor. The browser silently collects these values and resubmits automatically, since the data still has to travel from the browser back to the server, but there is nothing to ask permission for.
USER DEVICE PERMISSIONLESS SET ?device
AFTER EMIT ?device("screen_width")
AFTER EMIT ?device("user_agent")
USER DEVICE PERMISSIONS
Requests all six status-based permissions together — camera, microphone, geolocation, push, clipboard read, clipboard write — in a single popup. Does not include the screenshot, since it behaves differently and is requested on its own via USER DEVICE SCREENSHOT.
USER DEVICE PERMISSIONS LABEL "We need your permission to access your browser" SET ?device
Resolution, Not Approval
The script resumes once the visitor has answered every permission prompt — not once every permission has been granted. A browser can never be forced into universal acceptance. Each permission field reports one of the three real states the browser's own Permissions API returns:
| State | Meaning |
|---|---|
granted | The visitor allowed this permission |
denied | The visitor refused this permission |
prompt | Not yet decided — should not occur after USER DEVICE resumes, since resumption only happens once every prompt has been answered |
Full Field Reference
| Field | Description |
|---|---|
?device("permissions")("camera") | Camera access — granted / denied |
?device("permissions")("microphone") | Microphone access — granted / denied |
?device("permissions")("geolocation") | Location access — granted / denied |
?device("permissions")("push") | Push messaging — granted / denied. In the browser's own Permissions API this is tied to the notifications permission; push messaging has no separate permission of its own. |
?device("permissions")("clipboard_read") | Reading the system clipboard — granted / denied |
?device("permissions")("clipboard_write") | Writing to the system clipboard — granted / denied |
?device("screenshot") | The captured image itself if granted, or null if denied/cancelled — not a status field |
?device("screen_width") / ?device("screen_height") | Full screen dimensions — permissionless |
?device("viewport_width") / ?device("viewport_height") | Visible browser viewport dimensions — permissionless |
?device("pixel_ratio") | Device pixel density — permissionless |
?device("user_agent") | Browser user agent string — permissionless |
?device("language") | Browser locale — permissionless |
?device("timezone") | IANA timezone name — permissionless |
?device("online") | Current network connectivity state — permissionless |
?device("touch_support") | Whether the device supports touch input — permissionless |
?device("color_scheme") | Preferred light/dark color scheme — permissionless |
Reading Every Field
USER DEVICE LABEL "We need your permission to access your browser" SET ?device
AFTER EMIT ?device("permissions")("camera")
AFTER EMIT ?device("permissions")("microphone")
AFTER EMIT ?device("permissions")("push")
AFTER EMIT ?device("permissions")("geolocation")
AFTER EMIT ?device("permissions")("clipboard_read")
AFTER EMIT ?device("permissions")("clipboard_write")
AFTER EMIT ?device("screenshot")
AFTER EMIT ?device("screen_width")
AFTER EMIT ?device("screen_height")
AFTER EMIT ?device("viewport_width")
AFTER EMIT ?device("viewport_height")
AFTER EMIT ?device("pixel_ratio")
AFTER EMIT ?device("user_agent")
AFTER EMIT ?device("language")
AFTER EMIT ?device("timezone")
AFTER EMIT ?device("online")
AFTER EMIT ?device("touch_support")
AFTER EMIT ?device("color_scheme")
Checking a Permission Before Using It
A denied permission is a normal, expected outcome, not an error — always check its state before relying on it.
USER DEVICE LABEL "We need your permission to access your browser" SET ?device
AFTER IF ?device("permissions")("camera") IS EQUAL TO "granted"
OPEN
EMIT "Camera access granted — proceeding"
CLOSE
OR
OPEN
EMIT "Camera access denied — falling back to manual upload"
CLOSE
USER DEVICE uses the same halt-and-resume mechanism as GATHER and CONFIRM. It relies on a real visitor round-trip — rendering the popup, then resubmitting to the same script once every permission prompt has been answered. The plain credentialed API has no browser round-trip to resubmit against, the same isolation boundary documented on Session & Cookies.Syncing — Real-Time Exchange with a WebSocket Channel
A one-time request/response, even a resubmit-and-resume one like USER DEVICE itself, cannot keep a live, ongoing exchange running for as long as a page stays open. NEW SYNC CHANNEL opens a genuine WebSocket channel — the frontend and the running script can exchange data continuously, in either direction, without the page ever reloading.
NEW SYNC CHANNEL SET ?channel
AFTER EMIT ?channel("connect_url")
(* A raw WebSocket endpoint — the same connect_url shape already used by
NEW VIDEO BRIDGE and NEW AUDIO BRIDGE. No hosted UI, no bundled JavaScript —
you write your own client-side connection code. *)
SYNC CHANNEL ?channel SEND ?payload
SYNC CHANNEL ?channel RECEIVE SET ?data
(* Non-blocking — returns the next queued message immediately, or null
if nothing has arrived yet. Does not halt the script the way GATHER
or CONFIRM do; poll it repeatedly, or across separate calls inside a
START PERSIST timeline, to react to messages as they arrive. *)
SYNC CHANNEL ?channel CLOSE
How This Works Internally
One persistent Node.js relay service runs continuously as standing Ocalt infrastructure — the same always-on architecture already used by ocalt-core and ocalt-accounts, not a process spawned fresh per channel. NEW SYNC CHANNEL registers a new channel inside that one already-running relay and hands back a connect_url scoped to it. ql-runtime talks to the relay over its own private, internal-only HTTP API — the same pattern already used for ql-runtime’s calls to ocalt-core and ocalt-accounts — to send and retrieve messages on a channel. The developer’s own frontend JavaScript connects directly to the relay’s public WebSocket endpoint via the connect_url, exactly as it does for a Bridge call.
NEW VIDEO BRIDGE and NEW AUDIO BRIDGE, documented on Audio & Video Bridge | WebRTC, are built on this exact same relay — a video call is simply a sync channel whose messages happen to carry WebRTC signaling data instead of arbitrary application data.