Skip to content
Esc
↑↓navigate↵open⌘Jpreview
On this page

Browser features in your app

Camera, microphone, and location ask the viewer for each app, then the browser, and a chat host can block that prompt before it shows. A file picker asks for nothing, so it always works.

Which features work where

Feature Standalone app URL Rendered inline in a chat
Camera Works, once the viewer allows it for the app Blocked: the chat host denies the permission prompt
Location Works, once the viewer allows it for the app Blocked: the chat host denies the permission prompt
Microphone Works, once the viewer allows it for the app Blocked: the chat host denies the permission prompt
Screen capture, clipboard reading, MIDI Works, once the viewer allows it for the app Blocked
File picker Works Works
Fullscreen Works, once you claim the app Blocked
Pointer lock Works Blocked
alert, confirm, prompt The browser’s own dialogs Charming’s dialog; confirm and prompt return a Promise
Leaving for another site from a click Works, once you claim the app Blocked
Page routes with history.pushState The route shows in the address bar, links open it, and Back and Forward step through it Blocked: a path throws SecurityError; a # route works

Camera, location, microphone, screen capture, clipboard reading, and MIDI. These need no import and no claim. Charming finds the calls in the app’s code when it is published. At the standalone URL, the app’s first call opens a Charming dialog that asks the viewer whether this app may use the device. After Allow, the app restarts once and the next call gets the browser’s own prompt. Charming remembers the answer for that app in that browser, so another app asks again, and a location answer lasts across visits. Once the app has asked, the Charming widget gains a Device access tab that lists each answer with Reset. That tab appears even for an app that hides the widget, once the viewer has an answer to undo. Call window.charming.openDeviceAccess() from a button to take the viewer there; see the browser runtime reference. Chat hosts block the permission prompt. Charming shows a link to the standalone app when needed.

A refusal reaches the app as the browser’s own error: NotAllowedError from getUserMedia and the other promise APIs, or PERMISSION_DENIED from geolocation. Its message says whether the viewer blocked the device or dismissed the request. navigator.permissions.query reports prompt until the viewer answers and denied after a block. Resetting an allowed device restarts the app without it, which stops any capture in progress.

File picker. It needs no manifest import or claim, so it works in both hosts. See Data storage.

Fullscreen, pointer lock, dialogs, and leaving the page. These need no manifest import. Pointer lock and dialogs work in every app; fullscreen and leaving the page need a claimed app. Call requestFullscreen() or requestPointerLock() from a click. On the web, confirm() returns true or false; in a chat it returns a Promise, so write await confirm(...) if the app also runs there. A click handler can set window.top.location to send the visitor to a sign-in or checkout page; the same code without a click is blocked. When the provider sends the visitor back to the app, the app does not see the query string of that return URL, so check the result from the app’s backend.

Page routes. An app can have pages of its own. Change the route with history.pushState() or history.replaceState() and read it from location.pathname, which starts at / like a site on its own domain. On the web the route appears after the app’s URL, so /todos/3 in an app at charm.ing/ana/notes shows charm.ing/ana/notes/todos/3. Opening that link opens the app at /todos/3, and Back and Forward move between the app’s routes without reloading the page. The path and the #fragment go into the address bar. The query string stays with Back, Forward, and a reload but is left out of a shared link. Listen for popstate to redraw when the visitor goes Back. An app’s own Back button can call history.back(); it steps through the app’s routes and stops at the first one. In a chat the app has no path of its own, so an app that also runs there routes with location.hash instead.

Camera and microphone streams are device API results, not audio or video files. Playing an uploaded audio or video file needs no import; see External files and CSP.

Build so it works in every host

Call the normal Web API by name from a user action, and handle denial. Do not build a second permission message. For a file picker, use a normal file input and the storage API.

Technical

The table above is the host contract. The full build reference lists the exact capability imports.

Copy this prompt for your agent

Add a browser feature to my Charming app: camera, location, or
microphone. Read
https://charm.ing/docs/guides/browser-features.md first. Work out
which one I need from what I'm asking for, and build it so it falls
back cleanly when the permission is blocked. Ask me if it isn't
obvious which feature I want.
If you haven't built a Charming app before, read
https://charm.ing/docs/build-mcp.md (or build-http.md for the HTTP
API) for the app skeleton first.

How an agent performs this job

Call the matching Web API directly (navigator.mediaDevices.getUserMedia, navigator.geolocation.getCurrentPosition) so Charming finds it at publish, and handle NotAllowedError. No import is needed when the app’s own code makes the call. When Charming cannot see the call, keep the charming:browser/[email protected]-style import so the device is offered anyway. That covers a client dependency making it (a QR scanner package calling getUserMedia), a computed method name, or code loaded at runtime. A file picker needs no import.

The contract

Camera, location, microphone, screen capture, clipboard reading, and MIDI work at the standalone URL after the viewer allows them for that app; no claim is needed. Pointer lock and native dialogs work at the standalone URL for every app; fullscreen and click-started top-level navigation need a claimed app. Charming’s inline viewer uses an opaque frame without the per-app capability origin, so these delegated permissions and features stay blocked in chat, and alert, confirm, and prompt show Charming’s dialog instead. Use an Open in web fallback. On the web, pushState and replaceState accept any URL on the app’s own origin, resolved against the current route; another origin throws SecurityError. Charming keeps the address bar inside the app’s URL: a route whose first segment is a Charming endpoint under that URL (for example api or events under /app/<id>) keeps the address at the app’s URL.

Was this page helpful?