A plain-language guide for whoever picks this up next. No R, no jargon. By the end you should know what each piece does, who can do what, and how to push a change to the live app.
The GMD team writes canonical variable rules — the official guidance for how household survey variables should be harmonized across countries. A computer (an AI-assisted draft) proposes how a new survey's variables map to those rules. A human needs to look at each proposal and either accept it, send it back for revision, or block it.
The Human Review App is the website where humans do that. It's a small Shiny web app running on the World Bank's Posit Connect server. Reviewers open it in a browser, see a queue of items to review, and click through to approve, request changes, or reopen previously approved items.
Figure 1. Where the app fits: it sits between the drafts and the canonical rules.
Think of the system as four boxes that talk to each other. You can keep the whole thing running just by understanding what each one does.
Figure 2. The four boxes and the path data takes when a reviewer opens the app.
Holds all the source code, the variable drafts being reviewed, and the review records themselves. Branches matter: main holds drafts; the protected review branch holds review records.
The internal server that hosts the live web app. It also holds the secret credentials the app needs (e.g. the GitHub App key). Click Deploy on a content item to re-run the existing code — but to push new code you must use rsconnect::deployApp().
The web UI itself. Reads drafts from main, writes review records to review, enforces who can approve what. Talks to GitHub over the API using a GitHub App installation.
Whoever is reviewing. Connect tells the app their username; the app looks up their role in config/roles.yml and shows or hides the buttons they are allowed to click.
config/roles.yml, commit, redeploy".
The app enforces a small state machine. A variable moves through these states; only certain roles can move it forward.
Figure 3. The lifecycle of a single review item, with the role allowed for each transition.
Plain-English version of the same flow:
There are exactly three roles, defined in review-app/config/roles.yml. Each person is mapped to exactly one role (their username must match what Connect returns in session$user).
| Role | Can do | Cannot do |
|---|---|---|
| reviewer | Save / edit a draft body. Submit a draft into review. | Approve or send back. Reopen anything. |
| approver | Approve an item under review. Send it back for revision. | Edit the body. Reopen approved items. |
| administrator | Reopen an already-approved item. Assign work. (Implicitly: everything the others can do if they were also mapped.) | — |
Current map: 1 administrator (acastanedaa), 3 approvers (clakner, dmahler, mnguyen3), 15 reviewers. Edit review-app/config/roles.yml, commit, push, and redeploy to change it. There is no in-app way to edit roles — that's a deliberate safety choice.
config/roles.yml from its own bundle and looks that username up. If the username is missing, the app treats the visitor as having no role — they can browse but can't take any action. Never a default role.
This is the most important section for whoever takes over. Two facts that often surprise people:
main does NOT auto-deploy. GitHub is the source of truth, but Connect doesn't watch it. You still have to run the deploy command below.Every code or role change goes out with this recipe. It works on macOS or Linux, from any terminal that has R installed.
# 1. Make sure your working copy is clean and on main
cd /path/to/GMD-canonical-schema
git checkout main
git pull
# 2. Make your changes (edit code, edit roles.yml, etc.)
# 3. Commit and push
git add .
git commit -m "describe what you changed"
git push origin main
# 4. Ship to Connect (this is the magic command)
RENV_CONFIG_AUTOLOADER_ENABLED=FALSE Rscript -e '
rsconnect::deployApp(
appDir = "review-app",
appFiles = c("DESCRIPTION", "NAMESPACE", "app.R", "renv.lock",
"R/", "inst/", "config/"),
appName = "review-app",
appTitle = "review-app",
appId = "88dcd5b3-91b0-47fd-97b6-b1e420c683f2",
account = "acastanedaa",
server = "wbconnect",
lint = FALSE,
dependencyResolution = "library"
)'
RENV_CONFIG_AUTOLOADER_ENABLED=FALSE — skips the project-level renv autoloader so the rsconnect package is found in your user library.appId — the existing content item's GUID on Connect. Re-using this updates the existing item in place rather than creating a new one.server = "wbconnect" — the nickname of the Connect server, registered in your ~/.Rprofile or .Renviron with an API key.appFiles — exact list of files to include in the bundle. Keep this in sync with the project structure.dependencyResolution = "library" — tells rsconnect to trust the existing renv.lock instead of re-checking it against the local R library.Before any of the above works, the machine you're deploying from needs three things in ~/.Renviron:
CONNECT_SERVER=https://datanalytics-int.worldbank.org
CONNECT_API_KEY=your-personal-key-here
The API key comes from the Connect UI (your name → API Keys → Add). The first deploy also requires the rsconnect R package, installed once with:
Rscript -e 'install.packages("rsconnect")'
On macOS this writes to ~/Library/R/<arch>/<R-version>/library and works for every subsequent deploy.
review-app/config/roles.yml — add or remove the - identity: entry.Same as above — change their role: line from reviewer to approver, commit, push, redeploy.
R/<file>.R..pem to a secure location.Rscript -e '
suppressMessages(library(httr))
pem <- paste(readLines("/secure/path/new-key.pem"), collapse = "\n")
httr::PUT(paste0(Sys.getenv("CONNECT_SERVER"),
"/__api__/v1/content/88dcd5b3-91b0-47fd-97b6-b1e420c683f2/environment"),
httr::add_headers(Authorization = paste0("Key ", Sys.getenv("CONNECT_API_KEY")),
"Content-Type" = "application/json"),
body = jsonlite::toJSON(list(list(name="GITHUB_APP_PRIVATE_KEY", value=pem)),
auto_unbox=TRUE))
'
Same PUT shape as above; send an array of {name, value} objects.
git revert the offending commit, push, and run the deploy command again.| Symptom | Likely cause | Fix |
|---|---|---|
| App shows "no package called 'reviewapp'" | Old deployment still using library(reviewapp) instead of pkgload::load_all(). |
Re-run the deploy command. The current app.R uses pkgload::load_all(".") + run_app(). |
| App shows "review app adapter not configured" | One or more of the 7 GitHub env vars are missing. | Use the PUT to /environment above to set REVIEW_APP_GH_* and GITHUB_APP_* on the content item, then redeploy. |
| Visitor sees "Authenticated as ... not authorized" | Their Connect username is not in config/roles.yml. |
Add them to roles.yml with the right role, commit, push, redeploy. |
| Connect UI "Deploy" does nothing visible | That's by design — it re-runs the same bundle, so nothing changes. | Use the rsconnect::deployApp() command to push a new bundle. |
| Deploy command fails with "Library and lockfile out of sync" | rsconnect wants to verify renv.lock against a local library we don't use. |
The command already passes dependencyResolution = "library" — that flag must be present. |
| Deploy command fails with "packages installed from unknown source: reviewapp" | An old reviewapp installation sits in your user library. |
Run Rscript -e 'remove.packages("reviewapp")' once, then retry. |
| Term | Plain meaning |
|---|---|
| Bundle | A .tar.gz the server unpacks to run the app. Contains code + config. |
| Connect | The internal server that hosts the web app. |
| Deploy | Send a fresh bundle to the server and (re)start the app. |
| GitHub App | A "bot identity" the app uses to read drafts and write review records to GitHub without using anyone's personal token. |
| Golem | An R package framework for building Shiny apps as proper R packages. The review-app is a Golem app. |
| pkgload::load_all | An R function that loads an R package straight from source code without installing it. The app uses this so Connect doesn't need to install the local package. |
| renv.lock | A snapshot of every R package version the app needs. Connect reads this to install the right versions server-side. |
| rsconnect | The R package that talks to Posit Connect — registers servers, authenticates, uploads bundles. |
| Shiny | An R framework for interactive web apps. |