Examples and best practices¶
This page shows what a project on the hub looks like, with one example per kind, followed by the habits that keep deploys predictable.
A minimal Shiny app¶
This is the app the hub's own smoke test deploys: one file, with ggplot2 recorded in its lock file.
# app.R
library(shiny)
library(ggplot2)
ui <- fluidPage(
titlePanel("Hello from the hub"),
sliderInput("n", "Number of observations", min = 10, max = 500, value = 100),
plotOutput("hist")
)
server <- function(input, output) {
output$hist <- renderPlot({
ggplot(data.frame(x = rnorm(input$n)), aes(x)) +
geom_histogram(bins = 30, fill = "steelblue", colour = "white") +
theme_minimal()
})
}
shinyApp(ui, server)
Next to it is the renv.lock that renv::snapshot() wrote. Running hub deploy in the
directory builds and registers the app. The first deploy writes a hub.yaml, and that is where
you set the title and the groups.
A project from the template¶
hub::new_app("sales", template = "shiny") creates a layout that works for apps larger than
one file:
app.R entry point: theme, layout, app-level inputs, module wiring; no logic
modules/ Shiny modules, one file per module
R_helpers/ logic with no Shiny in it, callable outside a running app
www/ served at the web root: styles.css, images
files/ read from disk by R, never fetched by the browser
data/ data files the app ships with
_brand.yml colours and fonts, read by bslib
hub.yaml how the app appears and runs on the hub
CLAUDE.md the layout and conventions, for the coding agent
The agent reads the project's CLAUDE.md before it writes code in the project. The
conventions for one kind of project therefore live in its template rather than in a hub-wide
file. A customer's own templates in the overlay replace or add to the platform's.
An API that knows its caller¶
This plumber2 API, made from the plumber2 template, has one endpoint that only the finance
group may use:
#* @get /figures
function(request) {
if (!hub::in_group("finance", request)) plumber2::abort_forbidden()
read_figures()
}
hub deploy publishes it at https://apps.hub.<your domain>/api/sales-api/. From a Shiny app
or a pipeline on the hub, httr2::request(hub::api_url("sales-api")) calls it as the current
user without any credential. A script outside the hub calls it with the key that the API's
owner, a maintainer or an admin gets from hub apikey. On a hub set up to accept them, a token from the
identity provider works as well. See APIs.
A pipeline and the app that reads it¶
Create the pipeline with hub::new_app("sales-etl", template = "targets"), write a
_targets.R that writes its results to the pipeline's store, and add a schedule to hub.yaml:
The run at six every morning is incremental, as targets is on your machine, because the
store is a persistent volume. The dashboard that shows the results lists the pipeline under
uses:
A hub on a single host accepts uses: as it is. On a cluster it needs a storage class that
can share the store (Cluster).
The dashboard reads the results with
targets::tar_read(figures, store = hub::store("sales-etl")). In an IDE session this reads the
current project's _targets, and on the hub it reads the mounted store. When a run fails,
hub logs sales-etl shows the reason. On a hub with
monitoring, the admins also get an alert mail.
Tools for the coding agents¶
The mcp template contains a tools.R that returns ellmer::tool() objects. Once the server
is deployed, its tools appear in Claude Code and Posit Assistant for every colleague in its
access-groups, from their next session start. The hub checks the caller's groups on every
call, and the tools run with the server's own credentials, such as the secrets in its
.Renviron. The typical use is
a tool that queries the data warehouse, written once by the team that knows the schema. See
MCP servers.
Best practices¶
- Snapshot before deploying. Run
renv::snapshot()oruv lock. The build installs exactly what the lock file lists, so a package that is installed but not recorded is missing on the hub. - Install inside the project. Use
renv::install()with the project open. A package added in a plain session goes into the personal library and is not recorded. - Keep secrets in
.Renviron. Never put them in the code or inhub.yaml. The deploy stores the file as the app's secret and leaves it out of the image. For a value that is not in the project, such as a password only an admin knows, usehub env set. - Commit, then deploy. The deploy records the commit it was built from. If there are
uncommitted changes, it warns and marks the deploy in
hub history. For an important app,require-clean: trueinhub.yamlturns the warning into a refusal, andhub git binddeploys the app whenever a change is merged. - Read the user from the request. Use
hub::user(),hub::groups()andhub::in_group(), or theX-SP-UserIdandX-SP-UserGroupsheaders in Python, where the group names are upper case. Do not use theSHINYPROXY_*environment variables: they describe the container. - Build paths with
here. Do not use relative paths, because the working directory in an RStudio session differs from the one in the deployed container. - Use
system-packagesonly whenhub sysreqsmisses a library. The build resolves system libraries from the lock file. The list inhub.yamlis for anything it does not find. - Keep data out of the project directory. The upload includes everything in the directory, up to 200 MB. Store data on the home outside the project, in a pipeline's store, or in a bucket.
- Use warm containers for the app people open first thing in the morning. With
prestart: 1the app opens at once. Every deploy then restarts the apps server (about a minute), so leave it off for apps you redeploy often. - Grant access to groups.
access-groupsrefers to the groups the identity provider sends. Membership is managed in the identity provider, and the app follows it.