Developing Shiny web applications
csiapps wraps an existing Shiny app with CSIAPPS authentication and a
consistent CSI navbar/footer. The same wrappers exist in both languages:
ui_wrapper() and server_wrapper().
By default csiapps runs in sandbox mode, so a wrapped app can be developed
and run locally without any OAuth client credentials — the login redirect is
simulated (see Sandbox mode below and the dedicated
Sandbox mode article). The following environment variables are
only required for a real deployment, once sandbox mode is turned off:
CSIAPPS_CLIENT_ID— client ID for the application registered in CSIAPPS.CSIAPPS_CLIENT_SECRET— client secret for the application.CSIAPPS_REDIRECT_URI— URL the application redirects to after authentication.CSIAPPS_SCOPE— (optional) scope of the authentication request.
A starting app
Suppose you already have this Shiny app:
library(shiny)
df <- faithful[, 2]
ui <- fluidPage(
titlePanel("Old Faithful Geyser Data"),
sidebarLayout(
sidebarPanel(
sliderInput("bins", "Number of bins:", min = 1, max = 50, value = 30)
),
mainPanel(plotOutput("distPlot"))
)
)
server <- function(input, output) {
output$distPlot <- renderPlot({
bins <- seq(min(df), max(df), length.out = input$bins + 1)
hist(df, breaks = bins, col = "darkgray", border = "white")
})
}
shinyApp(ui = ui, server = server)
from shiny import App, render, ui
import numpy as np
app_ui = ui.page_fluid(
ui.panel_title("Old Faithful Geyser Data"),
ui.layout_sidebar(
ui.sidebar(ui.input_slider("bins", "Number of bins:", 1, 50, 30)),
ui.output_plot("dist_plot"),
),
)
def server(input, output, session):
@render.plot
def dist_plot():
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.hist(waiting, bins=input.bins(), color="darkgray", edgecolor="white")
return fig
app = App(app_ui, server)
To migrate this app into the CSIAPPS ecosystem, use the helpers below.
1. set_institute()
Specify which institute internal API calls (such as authentication redirects) target:
# one of "csiontario" or "csipacific" (the default)
set_institute("csiontario")
# one of "csiontario" or "csipacific" (the default)
csiapps.set_institute("csiontario")
2. check_secrets()
Confirm the required environment variables are set. Pass verbose=True to print
the values being checked (secrets masked). Outside sandbox mode it raises if a
required variable is missing, which makes it useful for debugging; in sandbox
mode it never raises and simply reports whether an access token was found.
check_secrets(verbose = FALSE)
csiapps.check_secrets(verbose=False)
3. Sharing globals with the server
R evaluates ui and server in separate scopes, so objects defined at the
top level are not always visible inside the server function. Wrap that
setup in global_wrapper() so it is evaluated in the global environment and
reachable by internal helpers:
global_wrapper({
df <- faithful[, 2]
})
Python has no equivalent, and needs none: a module-level assignment in your
app.py is already visible to the server function that closes over it.
Just define it before server:
waiting = [...] # module-level; the server closure sees it directly
global_wrapper() is therefore R-only — see the
parity checklist.
4. ui_wrapper() and server_wrapper()
Wrap the UI and server. These add the CSI navbar/footer, an auth-status line, and the authentication flow (a real OAuth2 redirect in production, a simulated login in sandbox mode).
# before
shinyApp(ui = ui, server = server)
# after
shinyApp(ui = ui_wrapper(ui), server = server_wrapper(server))
# before
app = App(app_ui, server)
# after
app = App(csiapps.ui_wrapper(app_ui), csiapps.server_wrapper(server))
Full migrated app
library(shiny)
library(csiapps)
# CSIAPPS setup
set_institute("csiontario")
check_secrets()
global_wrapper({
df <- faithful[, 2]
})
ui <- fluidPage(
titlePanel("Old Faithful Geyser Data"),
sidebarLayout(
sidebarPanel(
sliderInput("bins", "Number of bins:", min = 1, max = 50, value = 30)
),
mainPanel(plotOutput("distPlot"))
)
)
server <- function(input, output) {
output$distPlot <- renderPlot({
bins <- seq(min(df), max(df), length.out = input$bins + 1)
hist(df, breaks = bins, col = "darkgray", border = "white")
})
}
shinyApp(ui = ui_wrapper(ui), server = server_wrapper(server))
from shiny import App, render, ui
import csiapps
# CSIAPPS setup
csiapps.set_institute("csiontario")
csiapps.check_secrets()
app_ui = csiapps.ui_wrapper(
ui.panel_title("Old Faithful Geyser Data"),
ui.layout_sidebar(
ui.sidebar(ui.input_slider("bins", "Number of bins:", 1, 50, 30)),
ui.output_plot("dist_plot"),
),
)
def server(input, output, session):
@render.plot
def dist_plot():
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.hist(waiting, bins=input.bins(), color="darkgray", edgecolor="white")
return fig
app = App(app_ui, csiapps.server_wrapper(server))
A runnable Python example is in
examples/app.py
(shiny run --reload examples/app.py).
Waiting for login
In production, authentication is asynchronous: server_wrapper() redirects to
CSIAPPS, and the user's access token only becomes available once they return and
the token is exchanged. Any code that reads registration or warehouse data needs
that token, so it cannot run at app startup — it has to wait until login
completes.
csiapps handles this for you. The token is stored per session (never in a
process-global, so concurrent users never share one) and resolved reactively, so
the data helpers gate themselves:
- Called from a reactive context before login completes,
make_request(),fetch_org_options(),fetch_profiles(), andfetch_profile()cancel quietly (they do not error) and the surrounding reactive re-runs automatically once the token arrives. - Called outside a session (a script or the R console) with no token, they raise a clear "not authenticated" error.
The practical consequence: fetch data from inside the server, not while building the UI. A call placed in the UI definition runs once at startup, when no session and no token exist yet. Populate inputs from a reactive instead.
server <- function(input, output, session) {
# Fetch org options once the token is available, then fill the dropdown.
# make_request() gates itself, so this observer simply re-runs on login.
org_list <- reactive({
make_request("api/registration/organization")[["results"]]
})
observeEvent(org_list(), {
updateSelectInput(session, "org",
choices = vapply(org_list(), `[[`, "", "name"))
})
}
def server(input, output, session):
# fetch_org_options() gates itself until login, so this effect just
# re-runs when the token arrives. Start the input's choices empty.
@reactive.effect
def _load_orgs():
ui.update_select("org", choices=csiapps.fetch_org_options())
Gating your own work with token_ready()
If you have work that must wait for login but does not itself call a
csiapps data helper — for example, processing an uploaded file — guard it with
token_ready(). It returns whether a token is available and, read inside a
reactive context, re-fires the guard automatically once login completes.
processed <- reactive({
req(token_ready()) # wait for login, then proceed
req(input$upload)
read.csv(input$upload$datapath)
})
@reactive.calc
def processed():
req(csiapps.token_ready()) # wait for login, then proceed
req(input.upload())
return pd.read_csv(input.upload()[0]["datapath"])
Note
In sandbox mode the token comes from your environment at startup, so these guards pass immediately and the gating is invisible — it only matters once sandbox mode is off and a real, asynchronous login is involved.
Sandbox mode
The code above is written for production, but you do not need client
credentials or a running identity provider to develop it. In sandbox mode (the
default) server_wrapper() simulates the login redirect: instead of sending
the browser to CSIAPPS to authenticate, it seeds the session from the
CSIAPPS_ACCESS_TOKEN already in your environment.
Sys.setenv(CSIAPPS_ACCESS_TOKEN = "your-dev-access-token")
import os
os.environ["CSIAPPS_ACCESS_TOKEN"] = "your-dev-access-token"
With that token set, the wrapped app runs locally and the header loads your
real identity (/me) from the API, exactly as after a production login. The
token is used only to emulate the login — everything else (sport
organizations, athletes, warehouse records) is dummy, served from the local
sandbox. If no token is set, the app shell still renders but shows an
unauthenticated notice.
Note
Sandbox mode never touches real client data. Warehouse calls made through
make_request() are emulated locally, and registration reads
(fetch_org_options(), fetch_profiles()) come from the local dummy
registry. Only /me uses your real token — call set_institute() to match
the institute that issued it, or that lookup is rejected.
Deploying
Because the app code is identical in both modes, deploying is just a matter of turning sandbox mode off — no code changes required.
options(csiapps.sandbox = FALSE) # or set CSIAPPS_ENV=production
Then generate a manifest.json from the app directory so the deployment
target (Posit Connect / shinyapps.io) can reproduce your package
environment; regenerate it whenever dependencies change:
rsconnect::writeManifest()
csiapps.set_sandbox_mode(False) # or set CSIAPPS_ENV=production
Deploy the app as you would any Shiny for Python app (for example
shiny run, a container, or Posit Connect), with CSIAPPS_ENV=production
and the OAuth client credentials set in the deployment environment.