Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

Settings, Files & Folders

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.

await Settings.SetAsync(SettingsScope.Secure, "token", token, MyJson.Default.String);
var saved = await Settings.GetAsync(SettingsScope.Secure, "token", MyJson.Default.String);

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.

const files = new FilesBridge();
await files.write("data", "photos/cat.jpg", blob);
const entries = await files.list("data", { path: "photos" });
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:

public sealed class Shares(FolderRoots folders)
{
public PickedFolder Publish(string name, string path) => folders.Add(name, path, displayName: name); // absolute path, existing directory
public bool Unpublish(string name) => folders.Forget(name);
}

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:

bridge.AddAppBridge(startup: o => o.Arguments.Add("--autostart"));
Topic Details
Picker 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.
Settings/_bridge/settings5 requests · 0 events

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.

GET/_bridge/settings/{scope}#

The keys this web app has set in a scope, and whether the scope is encrypted at rest.

C#ISettingsBridge.ListAsyncTSSettingsBridge.list
ParameterInType
scopepathSettingsScoperequired
Response
200 SettingsList
SettingsList · 3 fields
scopeSettingsScopeThe scope listed.
encryptedbooleanWhether values in this scope are encrypted at rest on this device.
keysstring[]The keys this web app has set, in ordinal order.
DELETE/_bridge/settings/{scope}#

Removes every key this web app set in a scope, and nothing the native app set.

C#ISettingsBridge.ClearAsyncTSSettingsBridge.clear
ParameterInType
scopepathSettingsScoperequired
Response
204 No content
GET/_bridge/settings/{scope}/{key}#

A value exactly as it was written. Fails with 404 when the key is not set.

C#ISettingsBridge.GetValueAsyncTSSettingsBridge.getValue
ParameterInType
scopepathSettingsScoperequired
keypathstringrequired
Response
200 unknown
Errors
404Not found
PUT/_bridge/settings/{scope}/{key}#

Sets a key to any JSON value, at most 1 MB.

C#ISettingsBridge.SetValueAsyncTSSettingsBridge.setValue
ParameterInType
scopepathSettingsScoperequired
keypathstringrequired
Body
application/json · unknown
Response
204 No content
DELETE/_bridge/settings/{scope}/{key}#

Removes a key. Fails with 404 when it is not set.

C#ISettingsBridge.RemoveAsyncTSSettingsBridge.remove
ParameterInType
scopepathSettingsScoperequired
keypathstringrequired
Response
204 No content
Errors
404Not found
Files/_bridge/files12 requests · 1 events

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.

GET/_bridge/files#

The names of the roots the page can use.

C#IFilesBridge.GetRootsAsyncTSFilesBridge.getRoots
Response
200 string[]
GET/_bridge/files/{root}/list#

A directory's entries, directories first. Fails with 404 when it does not exist.

C#IFilesBridge.ListAsyncTSFilesBridge.list
ParameterInType
rootpathstringrequired
pathquerystringoptional
Response
200 FileEntry[]
FileEntry · 5 fields
namestringThe entry's name; the root's name for the root itself.
pathstringRelative to the root, with forward slashes. Empty for the root.
isDirectorybooleanWhether it is a directory.
sizenumber | nullBytes, for files.
modifiedstringWhen it last changed.
Errors
404Not found
GET/_bridge/files/{root}/info#

One file or directory. Fails with 404 when it does not exist.

C#IFilesBridge.GetInfoAsyncTSFilesBridge.getInfo
ParameterInType
rootpathstringrequired
pathquerystringrequired
Response
200 FileEntry
FileEntry · 5 fields
namestringThe entry's name; the root's name for the root itself.
pathstringRelative to the root, with forward slashes. Empty for the root.
isDirectorybooleanWhether it is a directory.
sizenumber | nullBytes, for files.
modifiedstringWhen it last changed.
Errors
404Not found
GET/_bridge/files/{root}/content#

A file's content as a stream. Fails with 404 when it does not exist.

readBytes / ReadBytesAsync — A file's content. Fails with 404 when it does not exist.

C#IFilesBridge.OpenReadAsync / ReadBytesAsyncTSFilesBridge.openRead / readBytes
ParameterInType
rootpathstringrequired
pathquerystringrequired
Response
200 Binary content — Blob in TypeScript, Stream / byte[] in C#
Errors
404Not found
PUT/_bridge/files/{root}/content#

Writes a file from a stream. With overwrite false, fails with 409 when it exists.

writeBytes / WriteBytesAsync — Writes a file. With overwrite false, fails with 409 when it exists.

C#IFilesBridge.WriteAsync / WriteBytesAsyncTSFilesBridge.write / writeBytes
ParameterInType
rootpathstringrequired
pathquerystringrequired
overwritequerybooleanoptional
Body
application/octet-stream — raw content, not JSON
Response
200 FileEntry
FileEntry · 5 fields
namestringThe entry's name; the root's name for the root itself.
pathstringRelative to the root, with forward slashes. Empty for the root.
isDirectorybooleanWhether it is a directory.
sizenumber | nullBytes, for files.
modifiedstringWhen it last changed.
Errors
409Conflict
PUT/_bridge/files/{root}/content#

Writes a UTF-8 text file. With overwrite false, fails with 409 when it exists.

C#IFilesBridge.WriteTextAsyncTSFilesBridge.writeText
ParameterInType
rootpathstringrequired
pathquerystringrequired
overwritequerybooleanoptional
Body
text/plain; charset=utf-8 — raw content, not JSON
Response
200 FileEntry
FileEntry · 5 fields
namestringThe entry's name; the root's name for the root itself.
pathstringRelative to the root, with forward slashes. Empty for the root.
isDirectorybooleanWhether it is a directory.
sizenumber | nullBytes, for files.
modifiedstringWhen it last changed.
Errors
409Conflict
POST/_bridge/files/{root}/append#

Appends UTF-8 text to a file, creating it if needed.

C#IFilesBridge.AppendTextAsyncTSFilesBridge.appendText
ParameterInType
rootpathstringrequired
pathquerystringrequired
Body
text/plain; charset=utf-8 — raw content, not JSON
Response
200 FileEntry
FileEntry · 5 fields
namestringThe entry's name; the root's name for the root itself.
pathstringRelative to the root, with forward slashes. Empty for the root.
isDirectorybooleanWhether it is a directory.
sizenumber | nullBytes, for files.
modifiedstringWhen it last changed.
POST/_bridge/files/{root}/append#

Appends bytes to a file, creating it if needed.

C#IFilesBridge.AppendBytesAsyncTSFilesBridge.appendBytes
ParameterInType
rootpathstringrequired
pathquerystringrequired
Body
application/octet-stream — raw content, not JSON
Response
200 FileEntry
FileEntry · 5 fields
namestringThe entry's name; the root's name for the root itself.
pathstringRelative to the root, with forward slashes. Empty for the root.
isDirectorybooleanWhether it is a directory.
sizenumber | nullBytes, for files.
modifiedstringWhen it last changed.
POST/_bridge/files/{root}/directory#

Creates a directory and its parents.

C#IFilesBridge.CreateDirectoryAsyncTSFilesBridge.createDirectory
ParameterInType
rootpathstringrequired
pathquerystringrequired
Response
200 FileEntry
FileEntry · 5 fields
namestringThe entry's name; the root's name for the root itself.
pathstringRelative to the root, with forward slashes. Empty for the root.
isDirectorybooleanWhether it is a directory.
sizenumber | nullBytes, for files.
modifiedstringWhen it last changed.
DELETE/_bridge/files/{root}/entry#

Deletes a file or a directory. A directory that is not empty fails with 409 unless recursive.

C#IFilesBridge.DeleteAsyncTSFilesBridge.delete
ParameterInType
rootpathstringrequired
pathquerystringrequired
recursivequerybooleanoptional
Response
204 No content
Errors
409Conflict
POST/_bridge/files/{root}/move#

Moves or renames a file or directory within a root.

C#IFilesBridge.MoveAsyncTSFilesBridge.move
ParameterInType
rootpathstringrequired
Body
application/json · FileTransfer
FileTransfer · 3 fields
fromstringThe entry to move or copy.
tostringWhere it goes, including its new name.
overwritebooleanReplace an existing file at to. Directories are never merged or replaced.
Response
200 FileEntry
FileEntry · 5 fields
namestringThe entry's name; the root's name for the root itself.
pathstringRelative to the root, with forward slashes. Empty for the root.
isDirectorybooleanWhether it is a directory.
sizenumber | nullBytes, for files.
modifiedstringWhen it last changed.
POST/_bridge/files/{root}/copy#

Copies a file, or a directory recursively, within a root.

C#IFilesBridge.CopyAsyncTSFilesBridge.copy
ParameterInType
rootpathstringrequired
Body
application/json · FileTransfer
FileTransfer · 3 fields
fromstringThe entry to move or copy.
tostringWhere it goes, including its new name.
overwritebooleanReplace an existing file at to. Directories are never merged or replaced.
Response
200 FileEntry
FileEntry · 5 fields
namestringThe entry's name; the root's name for the root itself.
pathstringRelative to the root, with forward slashes. Empty for the root.
isDirectorybooleanWhether it is a directory.
sizenumber | nullBytes, for files.
modifiedstringWhen it last changed.
Files1 event
EVENTfiles.roots#

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.

C#IFilesBridge.OnRootsChangedAsyncTSFilesBridge.onRootsChanged
Payload
FileRootsChanged
rootstringThe root's name.
changeFileRootChangeWhat happened to it.

JSON property names are camelCase and enums travel as their names. A ? marks a property the payload may omit;| null one that may be null.

SettingsScopeenum#

Which store a setting lives in.

"Local""Secure"
SettingsListobject#

The keys in a scope.

scopeSettingsScopeThe scope listed.
encryptedbooleanWhether values in this scope are encrypted at rest on this device.
keysstring[]The keys this web app has set, in ordinal order.
FileEntryobject#

A file or directory.

namestringThe entry's name; the root's name for the root itself.
pathstringRelative to the root, with forward slashes. Empty for the root.
isDirectorybooleanWhether it is a directory.
sizenumber | nullBytes, for files.
modifiedstringWhen it last changed.
FileTransferobject#

A move or copy within a root.

fromstringThe entry to move or copy.
tostringWhere it goes, including its new name.
overwrite?booleanReplace an existing file at to. Directories are never merged or replaced.
FileRootsChangedobject#

The files.roots event.

rootstringThe root's name.
changeFileRootChangeWhat happened to it.
FileRootChangeenum#
"Added""Replaced""Removed"