Persistent Data Dir¶
Every deployed app gets its own directory at <storage.app_data_dir>/<slug>/.
Use it for files the app reads (Parquet, DuckDB, SQLite) and for data the app
writes (uploads, session state). For configuration and secrets, use
environment variables instead. Computed results that can be
thrown away belong in the result cache, which does not count
against the quota.
How the app sees it¶
The path is exposed to the app process two ways:
SHINYHUB_APP_DATAenv var (absolute path)../data/symlink inside the app's working directory (or, underruntime.mode: docker, a bind mount to/app-dataplus a<workdir>/datasymlink).
The data dir survives deploys, restarts, and rollbacks. It is removed only when the app itself is deleted. Recreating an app with the same slug starts with a fresh, empty data dir.
Pushing data¶
Deploy bundles must not contain a data/ entry: the server rejects any bundle
whose first path segment is a file, directory, or symlink named data (a 422
with the offending path). Push data in separately:
shinyhub data push <slug> ./seed.parquet
shinyhub data push <slug> ./big.csv --dest datasets/2026.csv --restart
shinyhub data ls <slug>
shinyhub data rm <slug> stale.csv
The same operations are available from the UI under Settings -> Data.
Reading data back¶
data pull is the read half of data push. It writes to the basename of the
remote path in the current directory, or wherever --dest names, and refuses to
overwrite an existing local file unless you pass --force:
shinyhub data pull <slug> datasets/2026.csv
shinyhub data pull <slug> seed.parquet --dest /tmp/check.parquet
shinyhub data pull <slug> seed.parquet --dest - | sha256sum
Every pull reports the sha256 of the bytes that arrived, which is what makes it
a verification rather than a copy. After restoring a server from backup,
data ls shows that a file came back with the right name and size; comparing
this digest against the one data push reported when the file was uploaded is
what shows the file itself came back. A pull that is cut short writes nothing:
the download lands in a temporary file and is renamed only once it is complete,
so a truncated file never appears under the name you asked for.
With --dest - the file's bytes are the only thing on stdout and the summary
goes to stderr, so piping into a checksum stays clean.
Authorization¶
PUT and DELETE on /api/apps/:slug/data/*path require app manager rights
or platform admin / operator. So does GET /api/apps/:slug/data/*path,
the single-file download: contents sit with the same permission that can
overwrite or delete them.
GET /api/apps/:slug/data (the listing) is deliberately looser: it requires the
app's owner, an explicit member (any role), or a platform admin / operator.
Public or shared visibility alone is not enough: file listings can leak
business intent (q4-revenue.parquet) and are kept off the public surface even
when the app itself is public. An explicit viewer-member can therefore see that
q4-revenue.parquet exists and how big it is, but cannot read what is in it.
Downloads are recorded in the audit log as data.pull. It is the only data
action that changes nothing and is audited anyway, because the trail is there to
answer who saw an app's data, not only who altered it.
Files are always served as application/octet-stream with nosniff and an
attachment disposition. The dashboard is served from the same origin, so an
uploaded .html or .svg rendered inline would execute as the dashboard.
Quota¶
storage.app_quota_mb caps the combined on-disk footprint of the app's deploy
bundles (including retained versions and their runtime environments) plus its
data dir. Allow headroom for retained versions and the next deployment. The
check runs on every deployment upload and data PUT and is
overwrite-aware: replacing a 100 MB file with a 50 MB one always succeeds. Set
it to 0 to disable.
Concurrent writes¶
The persistent dir is safe for any number of concurrent readers. For concurrent writers, use a real database (Postgres or MySQL); local SQLite or DuckDB in read-write mode does not survive multi-process writes.