r2-explorer
Cloudflare's R2 Explorer template — R2-Explorer, a Google Drive-style interface over an R2 bucket: folders, drag-and-drop and multipart uploads, in-browser previews of images, PDFs, text, Markdown and CSV, in-place editing, metadata — running on Cloudflare Workers + Static Assets, with the bucket provisioned by Terraform through Orun, and packaged as an Orun baseline: one command takes a new product from nothing to live on stage and prod, in its own repository, under its own name — in ONE landing.
The explorer and its dashboard are upstream's, unchanged: the Worker mounts
R2-Explorer's app, and the build copies its prebuilt dashboard into the
Worker's Static Assets, its <title> set to the product's name. It ships
read-only, as upstream does, until its owner stores a sign-in; then it
asks for HTTP Basic authentication and allows changes. What the baseline adds
is everything needed to make a copy of it somebody else's, built the way
cirrus-baseline is since
its one-landing bootstrap. See
specs/epics/r2-explorer for the
design, the milestones and the as-built status.
Instantiating a product
git clone --depth 1 --branch <baseline-tag> https://github.com/sourceplane/r2-explorer
cd r2-explorer
orun new --blueprint repo-blueprint.yaml --out ~/sites/acme-files --run-hooks --resume \
--set reponame=acme-files --set productname="Acme Files" \
--set githuborg=acme-inc --set subdomain=<workers-dev-subdomain>
Or, once the platform's registry lists it, from the Orun console (Baselines →
r2-explorer → Build). Two phases: 01-product creates the
repository and lands the whole product as ONE pull request — intent, CI,
tooling, the R2 root, the explorer and its tests — and one convergence
applies the R2 bucket on stage and prod and then deploys the explorer bound
to it; 02-docs records the live deployment from probed facts.
BOOTSTRAP.md is the operator guide;
docs/phases/ has a page per phase.
The only connection a build needs is Cloudflare (with R2 enabled on the
account). The explorer works from the first deploy, read-only: the Worker
reaches its bucket through its R2 binding, so there is no storage key to
paste. Its sign-in — BASIC_AUTH_USERNAME and BASIC_AUTH_PASSWORD, two
optional Orun secrets the deploy pushes to the Worker as Worker secrets — is
the owner's to set after the build.
The new repository receives the product only — the app, its R2 root, its tests, its CI, its tooling, a README of its own and its operating contract — renamed throughout to the product's name, repository, Cloudflare Worker and bucket names. None of this repository's machinery ships.
The slug is also the upstream package's name. R2-Explorer is the npm
package r2-explorer, so the
rename engine renames this baseline's slug everywhere EXCEPT where the tree
names the package — the pinned dependency, the one import, the package's
directory the build copies from — and Cloudflare's template it was ported
from (tooling/rebrand/rebrand.mjs, UPSTREAM; held by
testing/rebrand.test.sh and testing/placement.test.sh).
| What makes it a baseline | Where |
|---|---|
| The console card (inputs, secrets, programme) | blueprint.yaml |
| The build document (modules, phases, hooks) | repo-blueprint.yaml |
| This tree's identity, and the bootstrap's task contracts | .baseline/ |
| The rename engine | tooling/rebrand/rebrand.mjs |
| The bootstrap's helpers (deferrals, redeploy marker, work-tree strip) | tooling/bootstrap/ |
| The deployment-record renderer | hooks/render-deployment-docs.sh |
| The product's README and context pack | .baseline/product/README.md, ai/context/ |
| The baseline's tests: contract, wiring, leak, offline bootstrap, end to end | testing/, .github/workflows/baseline.yml |
Cutting a baseline-vN tag | .github/workflows/tag.yml |
What is here (the product)
| Surface | Where |
|---|---|
| The Worker: R2-Explorer, configured per environment, and its health route | apps/web/src/index.ts, apps/web/src/config.ts, apps/web/src/health.ts |
The site's name (the dashboard's <title>) | apps/web/src/site.ts |
| The dashboard, copied from the package into Static Assets | apps/web/scripts/build-dashboard.mjs, apps/web/scripts/dashboard-title.mjs |
| Workers bindings and deploy contract | apps/web/wrangler.template.jsonc + wiring.fixture.json (→ the committed wrangler.jsonc), component.yaml |
| The sign-in's path to the Worker (Worker secrets) | tooling/wire/worker-secrets.mjs |
| The R2 bucket (stage, prod) | infra/terraform/cloudflare-r2 |
| The wrangler renderer | tooling/wire |
| Tests | tests/web (vitest: the configuration, the health route, the Worker through the real R2-Explorer over an in-memory bucket, the dashboard's title) |
Getting started
pnpm install
pnpm run wire:fixture # render apps/web/wrangler.jsonc from the offline fixture
pnpm typecheck
pnpm lint
pnpm test
pnpm build # the dashboard → apps/web/dist/dashboard
pnpm --filter @app/web dev # wrangler dev: the explorer on http://localhost:8787, a local R2 bucket
cp apps/web/.dev.vars.example apps/web/.dev.vars # set both keys to sign in and allow changes
How it maps to Cloudflare's template
| The template | Here |
|---|---|
src/index.ts: R2-Explorer({ readonly: true }) | The same app, its configuration read from the Worker's secrets: read-only with none, HTTP Basic sign-in and changes allowed with both (apps/web/src/config.ts) |
readonly: true edited by hand to allow uploads | Store BASIC_AUTH_USERNAME and BASIC_AUTH_PASSWORD and redeploy |
Cloudflare Access (cfAccessTeamName) | Left out: an Access application is a Zero Trust resource no Orun composition provisions — Basic auth instead |
Email Routing into the bucket (the email handler) | Left out: no composition for Email Routing; the Worker exports fetch only |
Assets served from node_modules/r2-explorer/dashboard | Copied into dist/dashboard by the build, its <title> the product's name; the same html_handling and single-page-application fallback |
A bucket created with wrangler r2 bucket create | infra/terraform/cloudflare-r2, applied by Orun before the Worker deploys, one bucket per environment |
npx wrangler deploy | Orun applies the bucket and deploys <repo>-web-{stage,prod} on merge to main (apps/web/component.yaml) |
| "Deploy to Cloudflare" (clone the template) | orun new --blueprint repo-blueprint.yaml — a new repository, branded, deployed |
npm, R2-Explorer 1.1.10, hono from its lockfile | pnpm, both pinned exactly (R2-Explorer 1.1.10, hono 4.13.8) |
How the bucket name reaches the Worker
infra/terraform/cloudflare-r2 ──apply──► orun secret WIRING_CLOUDFLARE_R2 (stage, prod)
(CLOUDFLARE_R2_TOKEN, r2-data) │ (each deploy resolves it as it starts;
│ a missing one fails the lane)
▼
apps/web dependsOn cloudflare-r2 (enforced on main) · secretEnv WIRING_CLOUDFLARE_R2_<ENV>
optionalSecretEnv BASIC_AUTH_USERNAME, BASIC_AUTH_PASSWORD
build: tooling/wire/live.mjs → wrangler.jsonc with the environment's bucket name
(dev, verify-only, renders the fixture) → the dashboard copied into place
→ tooling/wire/worker-secrets.mjs: the sign-in, when stored, to the Worker
deploy: CLOUDFLARE_API_TOKEN (workers-deploy) — the running Worker holds no storage key
wrangler.jsonc is committed as the fixture render of
wrangler.template.jsonc, because the deploy lane's verify-app-structure
step checks the config exists before it builds; testing/e2e.test.sh fails
if the two disagree.
CI
| Workflow | What it runs | Ships to a product |
|---|---|---|
ci.yml | the Orun pipeline, and the only workflow a product carries: orun plan --changed, then one job per lane. A pull request runs the verify lanes (the R2 root planned for stage and prod, the dashboard built and the Worker dry-deployed per environment, the tests run), except the bootstrap's own landings (orun/BASE-*), where plan alone runs and nothing waits for it (the landing checked its commit in the box); a merge applies the R2 bucket, then deploys stage → prod. It plans nothing while intent.yaml names the placeholder workspace ws_CHANGE_ME, as this repository does. A manual run (Orunbase's Deploy button, or Run workflow on main) plans the environment and components it names under the push-to-main trigger, so they deploy as a merge deploys them | yes |
baseline.yml | the baseline's own tiers: contract tests, the wiring path, the blueprint read by the pinned orun, the leak gate, the offline bootstrap with the product's own plan, the landing installed, type-checked, linted, tested and built, the product built in the job tree, dry-deployed and served — read-only, then signed in | no |
tag.yml | cuts a baseline-vN tag (manual) | no |
Credits
R2-Explorer is © 2023-2025 Gabriel Massadas, MIT
(npm, installed as a
dependency, its LICENSE with it). The template this port starts from is
Cloudflare's, MIT (© 2018 Cloudflare, Inc.; kept as apps/web/LICENSE).
Build from the console
Press Build: R2 Explorer goes into the workspace you choose (or a new one), you connect Cloudflare, and an agent builds it.
Build from the CLI
# check providers, then build
orun baseline check r2-explorer
orun baseline new r2-explorer --via-platform --repo acme/r2-explorer