Native services
Native services are built-in globals whose backend touches the host: the filesystem, OS notifications, the share sheet, the system tray. Each one is written so the same call works on web, desktop, and mobile. It reaches a real device API where one exists and degrades to a web surface where it doesn't. They fall into two tiers.
The two tiers
Universal vs. native
Universal services (fs, notify, share, geolocate, camera) exist on every target with a graceful fallback, so they always work and need no declaration. Native services (tray) reach a capability only some targets can provide and have no fallback - an app that uses one must declare it in native_services, and declaring it on a target that can't deliver it aborts boot with a clear message instead of failing at the first call.
# settings.rat - opt into a native-tier service
native_services: ['tray'] fs - local files
Universal: disk on native, in-memory on web
fs reads and writes files through one contract: save / append / read / read_lines / exists / remove. On a desktop or mobile build it writes real files (rooted at a per-OS app-data dir, or a jailed fs_root); on the standard web build it falls back to a non-durable in-memory store, so the same source still runs as a website. For the web build, fs.export(filename?) downloads the whole store as one JSON bundle and fs.import() loads a bundle back - explicit, user-gesture persistence (and a backup/restore feature on durable builds).
> save_note[text]
fs.append('notes.log', text + '\n')
<< fs.read('notes.log')
> backup[]
fs.export('my-notes.json') notify - OS notifications
Universal: native banner or in-page toast
notify.send(title, body) raises a real OS notification when the shell granted permission (desktop WebView2, mobile, and browsers that allow it), and otherwise shows a self-dismissing in-page toast. One call, a platform-appropriate surface everywhere.
> ping[]
notify.send('Build done', 'All tests green.') share - the share sheet
Universal: native sheet, Web Share, or clipboard
share.open(text, url) opens the native share sheet on mobile (and desktop where available); on the web it tries the Web Share API, then a clipboard copy, then a toast - so it always does something visible. The url argument is optional.
> share_link[]
share.open('Check out Rat', 'https://rat-lang.com') geolocate - device location
Universal: native GPS or browser geolocation
geolocate.get() starts a one-shot location request and returns immediately - the fix arrives asynchronously. On mobile/desktop with the native bridge it uses the device GPS; on the web it uses the browser's navigator.geolocation. Either way the result lands on the reserved __rat_geolocate channel as {lat, lng, accuracy} (or {error}), which your page subscribes to.
> locate[]
geolocate.get()
<< 'requested' camera - take a photo
Universal: native camera or getUserMedia
camera.capture() starts a capture and returns immediately; the photo arrives asynchronously. On mobile/desktop it opens the device camera through the native bridge; on the web it uses getUserMedia. Rather than push megabytes of image down the channel, the photo is saved into the app's fs store and only a lightweight {path: 'photos/<id>.jpg'} reference is broadcast on the reserved __rat_camera channel. Display it with an <img> pointed at the jailed /__rat__/fs/<path> route, which streams the bytes back from the store. Declare it with native_services: camera so the Android build requests the camera permission.
> snap[]
camera.capture()
<< 'requested' tray - the system tray
Native (desktop only): declare it
tray is the first native-tier service: a system-tray icon plus a menu, desktop-only. tray.menu([...]) sets the menu (each item is {id, label} or a bare string); clicking an item broadcasts {item: id} on the reserved __rat_tray channel - the same server-push model as notify - which your page subscribes to. tray.tooltip(text) sets the hover text. Because it's native-tier, an app that uses it must declare native_services: tray; that declaration aborts boot on web or mobile, where there is no tray.
# data/tray.rat
> service [tray_demo]
on_start[]
tray.tooltip('My App')
tray.menu([{id: 'open', label: 'Show window'}, {id: 'quit', label: 'Quit'}]) Declare what you use
Static detection
Forgetting to declare a native-tier service is easy to miss because it works on the target you're testing and silently no-ops elsewhere. At boot, Rat scans your source for native-tier usage: if you call tray.* without listing it in native_services, you get a warning naming the usage site and the fix, so a web or mobile build fails loudly via the declaration gate instead of quietly doing nothing.
[tier] native service "tray" is used (at service tray_demo.on_start)
but not listed in native_services - add native_services: ['tray'] These services pair with the build targets in Desktop & mobile apps: write the call once, declare any native-tier capability, and the same app runs as a website, a desktop window, and a phone app.
See also Desktop & mobile apps · Channels · Services