Build cross-platform desktop apps in R using Shiny and Electron.
amber is an R package that turns a Shiny app into a real, shippable
desktop application. It scaffolds the Electron project, wires up an R
runtime, applies a hardened security baseline, and produces native
installers — .dmg on macOS, .exe on Windows, .deb / .rpm on
Linux — all from a single init_project() call.
library(amber)
init_project(
project_name = "my-app",
project_description = "My Desktop App",
project_version = "0.1.0",
project_license = "MIT",
project_author = "Your Name",
project_email = "you@example.com",
window = list(width = 1400, height = 900, title = "My App")
)
start_app("my-app", watch = TRUE) # live-reload development
build_app("my-app") # platform-native installer- Supported platforms
- Prerequisites
- Installation
- Quick start
- Customizing the window
- Security model
- Code signing & notarization
- Project layout
- Public API
- Testing
- Continuous integration
- Roadmap
- Contributing
- License
| Platform | Status | R runtime | Installer artifact |
|---|---|---|---|
| macOS (arm64) | Supported | Bundled (CRAN) | .dmg / .zip |
| macOS (x86_64) | Supported | Bundled (CRAN) | .dmg / .zip |
| Windows (x86_64) | Supported | Bundled (silent NSIS install) | Squirrel .exe |
| Linux (x86_64) | Supported | System R | .deb / .rpm |
| Linux (arm64) | Experimental | System R | .deb / .rpm |
On macOS and Windows, init_project() downloads R from CRAN and unpacks
it into the project (r-mac/ or r-win/), producing a self-contained
distributable. On Linux, amber uses the end user's system R (bundling
portable Linux R is on the roadmap).
| Tool | Minimum version | Where |
|---|---|---|
| R | 3.6 | https://cloud.r-project.org/ |
| Node.js | 18 | https://nodejs.org/ |
| npm | 8 | bundled with Node.js |
Platform-specific extras:
- macOS: Xcode Command Line Tools (
xcode-select --install). - Windows: matching Rtools for your R version (only needed when
building source packages);
signtool.exeif you intend to sign releases. - Linux:
dpkg-debfor.deb,rpm-buildfor.rpm; thelibcurl4-openssl-dev/libssl-dev/libxml2-devtriplet for compiling common Shiny dependencies.
The check_nodejs(), check_npm() and check_electron_forge()
helpers verify the toolchain from R and offer to install Electron Forge
when it's missing.
# install.packages("remotes")
remotes::install_github("dm807cam/amber")Or with devtools:
devtools::install_github("dm807cam/amber")library(amber)
# 1. Verify the toolchain (once, optional)
check_nodejs()
check_npm()
check_electron_forge()
# 2. Scaffold a project
init_project(
project_name = "my-app",
project_description = "My Desktop App",
project_version = "0.1.0",
project_license = "MIT",
project_author = "Your Name",
project_email = "you@example.com"
)
# 3. Develop iteratively
start_app("my-app", watch = TRUE) # live reload on changes under shiny/
# 4. Build native installer
build_app("my-app")The starter Shiny app lives at my-app/shiny/app.R. Edit it, and with
watch = TRUE the R subprocess restarts and the window reloads on
save.
For supply-chain assurance on the bundled R installer, pin its SHA-256:
init_project(
...,
expected_sha256 = "0123abcd...64-character-hex..."
)amber aborts the project setup if the downloaded archive's hash does
not match.
init_project(
...,
window = list(
width = 1400,
height = 900,
title = "My App",
backgroundColor = "#ffffff",
minWidth = 800,
minHeight = 600,
autoHideMenuBar = TRUE,
resizable = TRUE
)
)Settings flow into amber-app.json next to package.json; the
Electron main process reads it at startup, so you never have to edit
JavaScript. Unknown keys are rejected with the supported list.
Every app produced by amber ships with a hardened baseline:
sandbox: true,contextIsolation: true,nodeIntegration: false,webSecurity: trueon everyBrowserWindow.- Renderer ↔ main IPC goes through a minimal
contextBridgesurface (amberSplash), exposing only the events the splash screen needs. - Strict per-URL Content-Security-Policy:
default-src 'none'for the local splash/error screens, and a Shiny-compatible CSP for the main window that still forbids any outbound network connection except to the local Shiny server. - Navigation is locked to the local Shiny origin via
will-navigate,setWindowOpenHandler, andwill-attach-webviewhandlers. External links route to the user's default browser viashell.openExternal. - A per-launch random 32-byte token is required as
X-Amber-Tokenon every request reaching the Shiny server, so other local processes can't reach it even if they guess the random port. setPermissionRequestHandlerandsetPermissionCheckHandlerdeny every Electron permission (geolocation, microphone, notifications…) by default.X-Content-Type-Options: nosniffandX-Frame-Options: DENYare applied to every response.
Signing is opt-in via environment variables — day-to-day development builds don't need any keys:
# macOS: sign + notarize
export AMBER_APPLE_IDENTITY="Developer ID Application: Your Org (TEAMID)"
export AMBER_APPLE_ID="you@example.com"
export AMBER_APPLE_ID_PASSWORD="app-specific-password"
export AMBER_APPLE_TEAM_ID="TEAMID"
# Windows: Squirrel signs the .exe with your .pfx
export AMBER_WIN_CERT_FILE="/path/to/cert.pfx"
export AMBER_WIN_CERT_PASSWORD="cert-password"build_app("my-app") then produces signed (and on macOS, notarized)
artifacts. forge.config.js reads these variables and silently skips
signing when they're unset.
A ready-to-use entitlements.plist for macOS hardened-runtime is
included in the template.
After init_project("my-app"):
my-app/
├── package.json # generated; pinned Electron 34
├── forge.config.js # per-platform makers + signing config
├── entitlements.plist # macOS hardened-runtime entitlements
├── r-info.json # which R strategy this build uses
├── amber-app.json # window customizations
├── r-mac/ # bundled R framework (macOS only)
├── r-win/ # bundled R install (Windows only)
├── library/ # project-local R library (Linux only)
├── shiny/
│ └── app.R # your Shiny app — edit this
├── start-shiny.R # token-authenticated Shiny launcher
└── src/
├── main.js # Electron main process
├── preload.js # IPC bridge
├── loading.html / .css # splash screen
└── loading.js
| Function | Purpose |
|---|---|
init_project() |
Scaffold a new Electron + R project |
start_app(watch=TRUE) |
Run in development mode (with live reload) |
build_app() |
Produce platform-native installer |
check_nodejs() |
Verify Node.js installation |
check_npm() |
Verify npm installation |
check_electron_forge() |
Verify (and offer to install) Electron Forge |
Browse ?amber after loading the package, or read the
getting-started vignette:
vignette("getting-started", package = "amber")# Run the unit-test suite (covers every pure function)
devtools::test()
# Or with testthat directly
testthat::test_dir("tests/testthat")The suite has 87 tests covering: project-name validation, OS dispatch,
window-config merging, install-spec selection, JSON quote safety,
run_cmd exit semantics, project-path checks, and platform-aware
install-hint formatting. Integration tests that need Node/network are
skipped on CRAN.
| Job | Runner |
|---|---|
lint-js |
rosegold (self-hosted) |
R-CMD-check macOS arm64 |
rosegold (self-hosted) |
R-CMD-check Linux x86_64 |
ubuntu-latest hosted |
R-CMD-check Windows x86_64 |
windows-latest hosted |
The full pipeline is defined in
.github/workflows/ci.yaml. See
docs/CI.md for runner registration, label conventions,
and the secret matrix for signing.
- Bundled R on Linux (currently relies on system R)
-
electricShine-style asset minification pass forlibrary/ - First-class
init_project(icon = "...")with per-OS conversion -
vignette("packaging")covering full release pipelines - Optional GUI launcher (Tkinter / Electron itself) for non-RStudio users to scaffold projects
Issues and pull requests are welcome. Before opening a PR:
- Run the unit tests:
devtools::test(). - Run
R CMD checkclean:devtools::check(). - If you touch the Electron template,
node --checkevery JS file you changed.
The CI workflow runs these checks for you on every push and PR.
MIT © Dennis Mayk. See LICENSE.md.