Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.Guaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans
AI Conversations, Speech, & Audio ReleaseHave a full conversation with AI, use speech commands, and interact with audio content seamlessly.Have a full conversation with AI, use speech commands, and interact with audio content seamlessly.
Both are built into the host and on by default (EnableSettings, EnableFiles).
Settings go through Shiny.Extensions.Stores. local is the platform’s settings store. secure is
its secure store: Keychain, Android KeyStore or DPAPI. On Linux it’s a plain file, and the listing’s
encrypted flag says so. Values can be any JSON and come back exactly as written. Keys are namespaced
by app id, so the page never sees or clears the native app’s own settings.
Files are confined to named roots: data (persistent) and cache (the OS may clear it), unless
you set FileRoots — plus any folder picked through the folders bridge or added by the app. Paths are relative and use
forward slashes. A path is refused if it contains .., \, :, a control character or anything a file name
can’t hold on some platform (< > " | ? *), or passes through a link that leads out of the root. Writes are atomic, parent
directories are created as needed, and MaxFileWriteBytes (256 MB) caps a single file — and sets the floor for the
server’s request body limit, whichever bridges are registered, so your own upload endpoints get it too.
await files.move("data", { from: "photos/cat.jpg", to: "photos/tabby.jpg" });
Folders the app maps while it runs — a folder chosen in its own dialog, a share an administrator published — go
through FolderRoots (Shiny.AppDeviceBridge.Folders). Each becomes a root the page uses by name, and is restored the
next time the app runs, on every platform:
The page never names a path — that’s the app’s decision — but it hears every change as the files.roots event
(IFilesBridge.OnRootsChangedAsync, FilesBridge.onRootsChanged). A folder that no longer opens stays in
FolderRoots.All with Available false. On Apple platforms a folder the app currently has security-scoped access to is
kept as a bookmark. Failures are WebAppFileException, with the code the folders bridge answers with.
Lower down, WebAppFileRoots holds every root and raises Changed. WebAppFileRoot is a directory on disk (an absolute
path); a store that isn’t one — an Android Storage Access Framework tree — implements WebAppFileStore, and
GetLocalPath returns null so path-only bridges refuse its files.
An app whose storage is a store like that can start with no roots at all: set DefaultFileRoots = false and, with
FileRoots left empty, the page gets neither data nor cache - add the app’s own store with WebAppFileRoots.Add
while the app runs. Configured FileRoots are used whatever the flag says.
Picking:POST folders/pick with { "root": "documents" } shows the platform’s folder picker. The folder
becomes a file root under that name — /_bridge/files/documents/… — and answers 204 if the user cancels.
Picking again under the same name replaces the folder. The app’s own roots can’t be replaced.
Remembered: picked folders come back as roots every time the app starts, until
DELETE folders/{root} forgets one. GET folders lists them, with available: false for one that was
moved, deleted or had its access revoked since.
Platforms: Apple platforms keep a security-scoped bookmark; Android keeps a persisted Storage Access
Framework grant; Windows and Linux (GTK’s file dialog) keep the path.
Folders the app adds by path go through FolderRoots.Add, not the page — see
Settings and files. They’re remembered the same way, and listed by GET folders beside the
picked ones.
Android folders aren’t paths. The files bridge reads and writes them through the Storage Access Framework,
but bridges that hand the OS a file path — sharing, transfers, notification images — refuse them.
Photos: photos never go in the JSON. hasPhoto says whether one exists, and
GET contacts/items/{id}/photo?size=full|thumbnail returns the image bytes.
Topic
Details
Writing
PUT changes only the properties you send. An empty list clears one.
iOS
reading note and relationships needs the com.apple.developer.contacts.notes entitlement. Without it they come back empty.
Calendar:
Topic
Details
Platforms
Android, iOS, Mac Catalyst, macOS and Windows.
Access
POST calendar/access takes ReadWrite (the default), ReadOnly or WriteOnly. Write-only access (iOS 17+) is reported as Restricted.
Listing
GET calendar/events requires start and end, at most 366 days apart. Paging works like contacts.
Reminders
reminderMinutes counts minutes before the start.
Read-only fields
attendees, the organizer and recurrence can be read but not written.
Read-only calendars
writing to one returns 403 read_only, and so does writing to a system calendar on Windows, which only allows writes to app-owned calendars.
Deleting
DELETE calendar/events/{id}?series=true removes the rest of a recurring series rather than one occurrence.
Mac Catalyst, sandboxed macOS
also need the com.apple.security.personal-information.calendars entitlement. Without it, access is denied without a prompt.
Both bridges return 403 access_denied until access has been granted.
Startup: part of the app bridge, over Shiny.Core’s IAppStartupInstaller. GET /_bridge/app/startup says
whether the app launches when the user logs in. Windows writes it under HKCU\…\CurrentVersion\Run
(unpackaged apps only — the OS virtualizes that key for MSIX), macOS 13+ submits the running bundle to
SMAppService, and Linux writes ~/.config/autostart/{Identifier}.desktop. Mobile has no such list, so it
answers { "supported": false, "state": "NotSupported" } and the rest return 501. state is read back from
the OS every time rather than remembered, because the user can turn a registered app off in Task Manager,
System Settings or Login Items without the app hearing about it — which is also why Enabled is not the only
success: DisabledByUser, DisabledByPolicy and RequiresApproval mean the user has to finish the job in the
OS, and POST app/startup/settings opens the screen where they do. Pass arguments your app can recognise on an
OS-started launch:
POST photos/pick shows the system photo picker — no permission needed — and copies what the user chose into a file root (cache by default) under photos/. The answer lists each as a { root, path } the page reads through the files bridge. An empty list means the user cancelled. Every head has a picker.
Library
browse every photo on the device, newest first: GET photos/library?offset=&limit= (200 at most), GET photos/library/{id}/thumbnail?size= for a JPEG that fits a square (32–1024 px), and POST photos/library/{id}/export to copy the original into a file root. It needs access — POST photos/access — and answers 403 without it.
Platforms
PhotoKit on iOS, Mac Catalyst and macOS; MediaStore on Android; the user’s Pictures folder on Windows. Linux has no photo library, so the library endpoints return 501 there.
Platform setup
NSPhotoLibraryUsageDescription on Apple platforms, plus the com.apple.security.personal-information.photos-library entitlement where the app is sandboxed. READ_MEDIA_IMAGES on Android 13 and later, READ_EXTERNAL_STORAGE before.
Key/value settings in the platform's stores. Values are any JSON; keys are 1–128 letters, digits, ., _, - or :. The typed getAsync1 and setAsync1 are what a page normally calls.
The page's own files, confined to named roots the app configured — data and cache by default. Paths are relative to the root and use /. Parent directories are created as needed, and a write that fails leaves no partial file behind.
A root was added, replaced or removed while the app ran — a folder the user picked or the app mapped, a share it published. Call getRoots again, and drop anything listed from a root that was replaced or removed.