shrd is a CLI-first file sharing service. Upload bytes from a terminal, get a URL, and download the same bytes somewhere else.
It is intentionally small:
- No accounts.
- No dashboard.
- No browser preview layer.
- One-year expiry by default.
- R2-backed direct and multipart uploads for large files.
- Optional client-side encryption for sensitive payloads.
Terminal transcript:
$ shrd upload ./report.pdf --name quarterly-report
-> https://shrd.stoff.dev/quarterly-report
$ shrd get quarterly-report --output ./downloads/
-> downloads/report.pdf
$ cat secret.env | shrd upload --mode private --expire 1h
-> https://shrd.stoff.dev/7kq4zn#key=...
$ shrd get 7kq4zn#key=... --raw --output -
DATABASE_URL=...The share URL itself serves file bytes as an attachment. /:id/raw stays available for scripts and compatibility.
Most ad-hoc sharing tools drift into accounts, dashboards, previews, and product surface. shrd keeps the useful part: reliable upload and download from the command line.
The implementation is designed to show a few senior engineering choices:
- Keep the hot path small: Workers route to KV or R2 without a UI server.
- Treat large uploads as a first-class path: direct upload first, multipart with resumable manifests past 95 MB.
- Make failure modes recoverable: idempotency keys, retryable CLI requests, delete tokens, and resumable multipart state.
- Avoid pretending text is the only payload: downloads preserve filename and content type where possible.
Homebrew:
brew tap Stoffberg/tap
brew install shrdFrom a fresh clone:
git clone https://github.com/Stoffberg/shrd.sh
cd shrd.sh
pnpm install --frozen-lockfile
cargo build --manifest-path cli/Cargo.toml --releaseRun the local binary:
./cli/target/release/shrd --helpUpload text, stdin, or a file:
shrd upload "inline text"
cat deploy.log | shrd upload --expire 1h
shrd upload ./archive.zip --name release-bundle
shrd upload ./large-video.movDownload content:
shrd get release-bundle --output ./downloads/
shrd get https://shrd.stoff.dev/release-bundle --raw --output -
shrd get last --open
shrd get release-bundle --metaUseful upload flags:
| Flag | Purpose |
|---|---|
--expire <value> |
Expiry as Nh, Nd, or never; default is 365d. |
--mode temporary |
Preset for short-lived shares; currently 1h. |
--mode private |
Enables client-side encryption. |
--mode permanent |
Sets expiry to never. |
--burn |
Delete after the first successful read. |
--encrypt |
Encrypt before upload; key stays in the URL fragment. |
--name <slug> |
Stable public slug, 4-64 URL-safe characters. |
--json |
Machine-readable upload result. |
--resume <manifest> |
Resume an interrupted multipart upload. |
Useful download flags:
| Flag | Purpose |
|---|---|
--output <path> |
Save to a file, directory, or - for stdout. |
--raw |
Write exact bytes without decoration. |
--open |
Save to a temp file and open with the default app. |
--copy |
Copy fetched text to the clipboard. |
--meta |
Print metadata JSON instead of downloading content. |
Local history is stored on the client so shrd get last, shrd list, and shrd search <term> work without an account.
The public API is deliberately narrow.
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Health check. |
POST |
/api/v1/push |
Compatibility path for JSON text uploads. |
POST |
/api/v1/upload |
Direct binary upload. |
GET |
/:id |
Download file bytes as an attachment. |
GET |
/:id/raw |
Download raw file bytes. |
GET |
/:id/meta |
Metadata needed by CLI download/decryption flows. |
DELETE |
/api/v1/:id |
Delete with Authorization: Bearer <deleteToken>. |
Direct upload:
curl -X POST https://shrd.stoff.dev/api/v1/upload \
-H "X-Filename: report.pdf" \
-H "X-Content-Type: application/pdf" \
-H "X-Expire: 365d" \
--data-binary @report.pdfResponse:
{
"id": "x7k2mz",
"url": "https://shrd.stoff.dev/x7k2mz",
"rawUrl": "https://shrd.stoff.dev/x7k2mz/raw",
"deleteUrl": "https://shrd.stoff.dev/api/v1/x7k2mz",
"deleteToken": "7hf...",
"expiresAt": "2027-06-10T12:00:00.000Z",
"name": null
}Multipart upload is used by the CLI for files over 95 MB:
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/multipart/init |
Create resumable upload session. |
GET |
/api/v1/multipart/:id/status |
Check uploaded parts. |
PUT |
/api/v1/multipart/:id/part/:partNumber |
Upload one 50 MB part. |
POST |
/api/v1/multipart/:id/complete |
Complete the R2 multipart upload. |
DELETE |
/api/v1/multipart/:id |
Abort a multipart session. |
Names must match ^[A-Za-z0-9][A-Za-z0-9_-]{3,63}$. api and health are reserved. Duplicate names return 409.
pnpm add @shrd/sdkimport { shrd } from "@shrd/sdk"
const uploaded = await shrd.upload(file, {
filename: "report.pdf",
contentType: "application/pdf",
expire: "365d",
})
const response = await shrd.download(uploaded.id)
const bytes = await response.arrayBuffer()Available methods:
| Method | Purpose |
|---|---|
push(content, options) |
Text compatibility upload. |
upload(body, options) |
Binary upload through /api/v1/upload. |
download(idOrUrl) |
Returns the Response for file bytes. |
pull(idOrUrl) |
Text convenience wrapper around download. |
createClient({ baseUrl }) |
Use a self-hosted API. |
CLI / SDK
|
| upload, multipart, download, metadata
v
Cloudflare Worker API (Hono)
|
+-- KV: small inline bodies and compatibility metadata
+-- R2: direct uploads and multipart objects
+-- D1: canonical metadata, delete tokens, idempotency, multipart state
Read path:
- Worker resolves metadata from D1 or the KV compatibility record.
- Small inline content streams from metadata/KV.
- File content streams from R2.
- Response includes
Content-Type,Content-Disposition, and cache headers.
Write path:
- CLI chooses text, direct upload, or multipart based on input size.
- API validates expiry/name/idempotency.
- Content lands in KV or R2.
- Metadata records filename, content type, encryption, expiry, size, and delete token.
| Decision | Rationale |
|---|---|
| Rust CLI | Fast startup, single binary distribution, reliable streaming for large files. |
| Cloudflare Workers | Low-latency global API with simple deployment and cheap scale. |
| R2 for files | Large object storage without egress fees. |
| KV for tiny content | Cheap, fast reads for small text compatibility uploads. |
| D1 for metadata | SQL constraints and durable state for idempotency, names, multipart sessions, and deletes. |
| No web app | The product is file transfer. Browser rendering is intentionally out of scope. |
Fresh clone:
pnpm install --frozen-lockfile
pnpm ci:full
pnpm --filter @shrd/api test:integrationProduction smoke check:
SHRD_API_URL=https://shrd.stoff.dev node scripts/smoke-prod.mjsUseful focused commands:
pnpm --filter @shrd/api test:unit
pnpm --filter @shrd/api test:compat
pnpm --filter @shrd/api test:e2e
cargo test --manifest-path cli/Cargo.toml --locked
cargo fmt --manifest-path cli/Cargo.toml -- --checkRun the API locally:
pnpm --filter @shrd/api devPoint the CLI at a local or self-hosted API:
SHRD_BASE_URL=http://127.0.0.1:8787 cargo run --manifest-path cli/Cargo.toml -- upload ./README.md
shrd config set-url https://shrd.example.comCreate Cloudflare resources:
wrangler d1 create shrd-db
wrangler kv:namespace create CONTENT
wrangler r2 bucket create shrd-storageCopy and edit the Worker config:
cp apps/api/wrangler.toml.example apps/api/wrangler.tomlApply migrations and deploy:
cd apps/api
wrangler d1 migrations apply shrd-db --remote
wrangler deployEncrypted uploads use AES-256-GCM locally before bytes leave the client. The key is placed in the URL fragment, so the API never receives it. Without that fragment, the server only has encrypted bytes.
Delete tokens are returned once at upload time. Anyone with the token can delete the share; without it, shares expire naturally.
MIT