Northeast Ohio Home Values: An App with Independent Data Updates
Question
How can a Shiny app use periodically updated data without being redeployed each time the data changes?
An app can query a database or API directly, or carry a static dataset in its deployment bundle. Slowly changing data offers another option: prepare a snapshot separately, publish it somewhere accessible, and let the app read it.
Experiment
I built a Northeast Ohio Home Value Explorer using public Zillow Research data. Visitors compare up to four of ten local communities, choose a starting year, and switch between estimated typical home values and percentage growth.
Zillow city CSV
-> publish-data.R: select, reshape, validate
-> board/: versioned pin and _pins.yaml manifest
-> Quarto copies resources into docs/
-> commit and push -> GitHub Pages deployment
-> shinyapps.io app reads the public pin per visitor session
Prepare and pin locally
The publisher downloads the all-homes, mid-tier, smoothed, seasonally adjusted Zillow Home Value Index (ZHVI). It selects ten Ohio communities and reshapes the monthly columns into community, date, and value. Checks require the requested communities, unique community/month pairs, valid dates, and positive nonmissing values. Missing observations are preserved.
The essential artifact handoff is:
board <- pins::board_folder(board_path, versioned = TRUE)
pins::pin_write(
board,
housing_long,
name = "home-values",
type = "rds",
metadata = list(source_url = source_url)
)
housing_pinned <- pins::pin_read(board, "home-values")
stopifnot(identical(housing_long, housing_pinned))
pins::write_board_manifest(board)These are reference snippets; the complete publisher is publish-data.R. RDS preserves R column types, including Date. The round-trip check verifies that the saved artifact matches the prepared object.
The board is the storage folder; home-values is a named pin within it:
board/
_pins.yaml
home-values/
<version>/
data.txt
home-values.rds
Make the board accessible online
Writing a pin creates local files; it does not publish them. A web reader cannot inspect a directory like a local filesystem, so _pins.yaml lists the pins and their version locations. Regenerate this manifest after writing pins.
The root _quarto.yml includes:
project:
resources:
- .nojekyll
- experiments/housing-pins/board/These entries tell Quarto to copy the board and the empty .nojekyll file into docs/. The board files are copied unchanged, rather than rendered as HTML. .nojekyll tells GitHub Pages to skip Jekyll processing so underscore-prefixed files such as _pins.yaml are served too.
Rendering, committing, pushing, and waiting for GitHub Pages to deploy makes the copied board accessible over HTTPS. The resource configuration works independently of this write-up’s draft status. The publisher also makes RDS files writable after pinning so Quarto can replace copied files on Windows.
Read the snapshot in Shiny
The app’s server() function contains:
web_board <- pins::board_url(
"https://mpfoley73.github.io/ds_lab/experiments/housing-pins/board/"
)
housing <- pins::pin_read(web_board, "home-values")Inside server(), this runs once per new visitor session. Existing sessions keep their snapshot. Outside server(), loading would happen at R process startup, so subsequent visitors could keep receiving older data. Process restarts are not our refresh mechanism. Pins caching can avoid unnecessary downloads; this experiment has not measured cache behavior separately.
The controls filter the session’s data and redraw the chart without rereading the pin. Growth uses the first month on or after the selected start with values for every selected community. Each line uses its own value in that shared month:
plot_data <- selected_data |>
filter(date >= baseline_date) |>
arrange(community, date) |>
mutate(.by = community, value = 100 * (value / first(value) - 1))Every growth line starts at zero. The shared-baseline requirement applies only to growth mode; dollar comparisons do not require overlapping observations.
What I learned
Preparing data, publishing data, and deploying app code are separate actions. The app and artifact do not need to share a hosting platform. This recreates the separation I previously used with pins on Posit Connect, using platforms I already had: GitHub Pages for public data and shinyapps.io for the app. Connect-specific authentication and scheduling were not part of this experiment.
| Change | Workflow |
|---|---|
| New housing data | Run publish-data.R; render and review the copied board; commit and push; verify the public pin after Pages deploys |
| New app behavior | Run deploy-app.R, which deploys only app/ to shinyapps.io |
| New write-up content | Render, review, and publish through the normal ds_lab website workflow |
Run scripts from the repository root. Follow the root README for website publication; running the publisher alone does not update GitHub Pages.
Verified: the initial snapshot contains 3,210 observations across ten communities through September 2026. Local and public pin reads reproduced the prepared data exactly. The deployed app works with the public board. Review also checked percentage calculations, selection limits, and a missing initial observation shifting the common baseline.
Pending: when October observations become available, publish a new snapshot and open a new app session without redeploying the app. Confirm the later data month appears. That will complete the independent-update demonstration.
ZHVI is a modeled estimate of typical home value, not an individual appraisal or the median price of homes sold. Historical estimates can be revised. This is a small public-data proof of concept: no scheduler, database, automatic refresh of open sessions, or elaborate recovery system.
For the reusable hosting pattern, see the pins web-hosted boards guide.