Lightweight file-hosting backend on Cloudflare Workers, built on
files-sdk so the storage layer is provider-agnostic
(R2 today; any files-sdk adapter later). Successor to the R2 upload scripts in
buildinternet-skills/github-screenshots.
Active development — not production-ready. uploads.sh is being built in the open and its APIs (including auth) will change without notice. Don't rely on it for anything you can't afford to lose or re-key.
pnpm install
cp apps/api/.dev.vars.example apps/api/.dev.vars
pnpm workspace:add default --local
pnpm dev # API on :8787; pnpm dev:web for the siteUpload a file:
curl -X PUT http://localhost:8787/v1/default/files/test.txt \
-H "Authorization: Bearer $UPLOADS_TOKEN" \
-H "Content-Type: text/plain" \
--data-binary "hello"The @buildinternet/uploads package wraps the API for scripting and GitHub
image embeds. pnpm workspace:add prints a bearer token once — save it to
.env (from .env.example) or run pnpm uploads setup --token <token>.
cp .env.example .env # fill in UPLOADS_TOKEN from workspace:add output
pnpm uploads put ./shot.png --env-file .env
# stdout: public URL + ready-to-paste markdown; stderr: human summaryHow keys work: default put is the fast path — run it with no flags and you get
a unique public URL (screenshots/<repo>/<date>/<name>-<hash>.<ext>). Use --pr or
--issue when you need a stable, hash-free filename for a GitHub embed. Use --key
only when you want an exact path.
More output control:
pnpm uploads put ./shot.png --format url --env-file .env
pnpm uploads put ./shot.png --repo myorg/myapp --ref 1722 --width 700 --env-file .envGitHub's native image hosting only works through a browser session — agents
and gh need a public URL first. The CLI uploads to R2 and returns stable
markdown you can drop into a PR or issue.
Stable PR/issue attachments (--pr / --issue) use hash-free keys so
re-uploading the same filename overwrites in place and the URL never changes:
pnpm uploads put ./after.png --pr 123 --alt "Dashboard after" --env-file .env
# key: gh/<owner>/<repo>/pull/123/after.pngManaged attachments comment (--comment, requires local gh auth)
creates or updates a single comment listing every file attached to that
PR/issue — the upload still succeeds if gh is unavailable:
pnpm uploads put ./after.png --pr 123 --comment --env-file .env
pnpm uploads comment --pr 123 --env-file .env # re-sync without uploadingPrivacy: Hosted files are served from a public CDN with no link to GitHub repo visibility. A screenshot on a private PR is still reachable by anyone who knows or guesses the URL —
--pr/--issuekeys embed the repo path and filename (gh/myorg/myapp/pull/123/after.png), so generic names are easier to guess than hashed keys. Treat uploads as public; don't host secrets or sensitive UI. Tighter access controls for private repos are planned — see roadmap.
See skills/uploads-cli/SKILL.md for agent-oriented usage and
docs/api.md for REST routes.
apps/api Hono worker — REST API, deploys to api.uploads.sh
apps/web Astro placeholder — future browse/manage UI
packages/storage @uploads/storage — files-sdk adapter factory
packages/uploads @buildinternet/uploads — CLI + client for GitHub image embeds
skills/uploads-cli Agent skill for driving the CLI
The API and web app are separate deployables. All storage access goes through
createStorage() in packages/storage — adding a provider is one new case
plus peer deps, no API changes.
| Doc | Contents |
|---|---|
| workspaces | Multi-tenant model, registration, BYO-bucket |
| admin-tokens | Minting, listing, and revoking upload tokens |
| api | REST routes and CLI usage |
| deploy | Cloudflare setup and production deploy |
| roadmap | Planned features |
Agent and contributor conventions live in AGENTS.md.