Shiny-style authoring, stateless API deployment.
aurora lets you build a web app the way you’d build a Shiny app — author the UI in R with bslib, scaffold the project, run it locally with hot-reload — but it compiles the UI to a static index.html at build time and lets plumber2 serve that UI plus your JSON API. There’s no reactive server, no per-user R process, and no sticky sessions: the app is stateless and scales horizontally behind a load balancer, ShinyProxy, or plain Docker.
- 🎨 UI in R — author with bslib +
_brand.yml; ship a static HTML asset. - ⚡ Stateless — plumber2 serves the assets and JSON routes; scale by adding containers.
- 🧩 Convention over config — fixed folders, no required manifest.
- 🔐 Batteries, opt-in — JWT-cookie auth, a hot-reloading data store, OpenTelemetry.
- 🐳 One-command deploy — generate a Dockerfile (
debianoralpine) or a ShinyProxy spec.
What an app looks like
A route handler is just an annotated function — its URL is the annotation:
# routers/iniciativas.R -> GET /api/iniciativas/data
#* @get /api/iniciativas/data
#* @serializer json
function() {
dados <- aurora_data_get(store, "iniciativas") # hot-reloaded from disk
list(data = dados, municipios = aurora_unique(dados$municipio))
}The UI is plain bslib, compiled once to www/index.html:
# build_ui.R -> www/index.html (static, built by aurora_build_ui())
build_ui <- function() {
bslib::page_fillable(
theme = bslib::bs_theme(version = 5, brand = TRUE),
aurora_component("/api/iniciativas/data", id = "tabela") # wires a div to the endpoint
)
}build_ui.R (bslib + _brand.yml) ──aurora_build_ui()──▶ www/index.html (static)
Browser ◀──▶ plumber2 server (aurora_app)
├── serves www/ (static UI + core.js/app.js)
├── routers/*.R → JSON
├── helpers/*.R (sourced first: config, db, auth, store)
└── optional JWT-cookie auth guard
Installation
# From GitHub — latest release (recommended):
# install.packages("pak")
pak::pak("aurora-govpe/aurora-rpkg@v0.1.5")
# Development version (main branch):
pak::pak("aurora-govpe/aurora-rpkg")
# From CRAN (once published):
install.packages("aurora")
remotes/devtoolswork too:remotes::install_github("aurora-govpe/aurora-rpkg@v0.1.5").
Quick start
library(aurora)
aurora_create_app("meu_app", template = "minimal")
aurora_add_route("iniciativas", dir = "meu_app") # -> routers/iniciativas.R
aurora_run("meu_app") # http://127.0.0.1:8000 (docs at /__docs__)aurora_run("meu_app", watch = TRUE) rebuilds the static UI on change. Theming is a _brand.yml consumed by bslib; auth, data, and telemetry are opt-in (template = "auth", aurora_data_store(), aurora_run(otel = TRUE)).
Canonical app layout (convention; no manifest required)
<app>/
├── api.R # entry: aurora::aurora_run("."), host/port from env
├── build_ui.R # defines build_ui() -> htmltools tag; sources ui_modules/
├── helpers/ # *.R sourced before routers are parsed (config, db, auth…)
├── routers/ # plumber2 annotated handlers; URL = annotation path
├── ui_modules/ # ui_*.R partials
├── www/ # static assets: js/core.js (runtime), js/app.js, style.css, images/
├── data/config.yml # app config (config package)
├── _brand.yml # optional: bslib theming (color/type/logo)
├── _aurora.yml # optional: overrides name/engine/auth/packages/attach/statics
└── Dockerfile # generated by aurora_dockerfile()
Deploy
aurora_dockerfile("meu_app") # flavor = "debian" (default)
aurora_dockerfile("meu_app", flavor = "alpine") # tiny image, source-built
aurora_build_image("meu_app", tag = "org/meu_app:latest", push = TRUE)The container serves the prebuilt static UI, so neither flavor installs bslib/shiny at runtime. aurora_build_image() targets linux/amd64 by default, so images built on an Apple Silicon Mac run on x86-64 servers (pass platform = NULL for a host-native build).
debian (default) — rocker/r-ver
|
alpine — rhub/r-minimal
|
|
|---|---|---|
| R packages | prebuilt binaries (Posit Package Manager) |
compiled from source (installr) |
| Build speed | fast | slow (full compile) |
| Image size | large (~1 GB+) | small |
| Architectures | amd64 binaries (arm64 compiles) | builds natively on amd64 and arm64 |
| System deps |
apt, broad/easy (glibc) |
curated apk, must compile (musl) |
Rule of thumb: debian for fast CI and rich/geo dependencies; alpine when image size matters most and you can absorb longer builds.
Apps that share static assets (logo, JS libraries, CSS) can serve them from one server-side directory via _aurora.yml:
# _aurora.yml — serve /srv/aurora-shared at /assets (alongside www/ at /)
statics:
/assets: /srv/aurora-sharedMount the directory as a volume (-v /srv/aurora-shared:/srv/aurora-shared:ro) and every app picks up changes from one place. See vignette("deploy").
Core functions
| Function | Purpose |
|---|---|
aurora_create_app() |
Scaffold the canonical layout (minimal / auth template) |
aurora_add_route() |
Generate an annotated plumber2 router in routers/
|
aurora_build_ui() / aurora_app() / aurora_run()
|
Compile UI, assemble the API, run locally (incl. attach= to load _aurora.yml packages and on_exit= for shutdown cleanup) |
aurora_config() |
Read data/config.yml anchored to the app root (no cwd pitfall) |
aurora_check() |
Lint the app: UI code in runtime helpers, undeclared packages, missing UI |
aurora_component() |
Emit a UI element wired to a JSON endpoint (data-endpoint) |
aurora_unbox() / aurora_geojson() / aurora_unique()
|
NULL-safe JSON response helpers (unbox / sf→GeoJSON / sorted-unique) |
aurora_data_store() |
Globals-free, hot-reloading data store for handlers |
aurora_auth_jwt() + aurora_jwt_* / aurora_*_auth_cookie()
|
JWT-cookie auth scheme (pluggable) |
aurora_dockerfile() / aurora_build_image() / aurora_shinyproxy_yaml()
|
Deploy: Docker + ShinyProxy |
See the Get started article, the Gallery, and the function reference.
Design decisions
- plumber2 only. Each handler’s URL comes from its annotation.
-
Thin JS runtime.
core.jsis aurora’s basics;app.jsandaurora_component()keep rendering in the app, not the package. -
Pluggable auth. A JWT-cookie
@headerguard, never inaurora_app()’s core path. -
Convention over manifest. Fixed folders;
_aurora.ymloptional. -
Theming via brand.yml. bslib consumes
_brand.ymlat build time.
Authors
- Andre Leite — maintainer (leite@castlab.org)
- Marcos Wasilew · Hugo Vasconcelos · Carlos Amorin · Diogo Bezerra · Júlia Nascimento Barreto
Developed by the NGR-Dados team (SEPLAG-PE) with UFPE.