Vibecode WeTransfer
track this build5 phases, 10 steps, beginner friendly0%Upload to object storage, mint a link, expire it. With client-side encryption you get the privacy WeTransfer never had. Their moat is bandwidth at scale and the brand a client trusts when a link arrives; for your own sends, a $5 bucket and a page does it.
You are building a lean indie version of WeTransfer. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # WeTransfer · indie build Large-file transfer on a bucket you pay cents for: uploads go straight to object storage with presigned URLs, links expire and count downloads, optional passwords, and optional client-side encryption with the key in the URL fragment so the server never sees it. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Storage | Cloudflare R2 or Backblaze B2 with presigned URLs | files never pass through your server | | Runtime | Node 22, node:http and node:sqlite | link records and the page | | Encryption | Web Crypto in the browser, optional | privacy WeTransfer never had | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **An S3-compatible bucket with access keys and CORS** · cents to a few dollars a month - Why: Direct browser uploads need CORS on the bucket. - Get it: R2: create bucket, API token, and set CORS to allow PUT from your domain. B2: similar under bucket settings. - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. The page and link table need a small always-on process. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **A domain or subdomain** (optional) · roughly $10 a year, or free on an existing domain - Why: send.yourdomain.com - Get it: Register at Cloudflare Registrar, Porkbun or Namecheap, or use a subdomain of one you already own. You add one DNS record in the deploy phase. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir send && cd send && git init && npm init -y && npm pkg set type=module && npm install @aws-sdk/client-s3@3 @aws-sdk/s3-request-presigner@3 mkdir -p data && cp .env.example .env ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. - bandwidth and speed on their CDN worldwide - the brand a recipient recognises - email delivery of the link and download receipts - the mobile apps and Paste If one of those is essential to you, that is the reason to keep paying for WeTransfer, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · WeTransfer The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a large-file transfer service like WeTransfer for my own sends. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. S3-compatible storage (Cloudflare R2 or Backblaze B2) with presigned URLs so files never pass through your server. Vanilla JS for the upload page. ### Data model (create this before Phase 1) - transfers: id (long random slug), created_at, expires_at, downloads, max_downloads, password_hash (nullable), note - files: id, transfer_id, key, name, size, content_type Object keys are random, never the original filename. ### Phase 1 · Upload Build: the page requests a presigned PUT per file and uploads directly to the bucket with progress; multipart for files over 100 MB; the server records the transfer and files. Cap total size from .env. Done when: a 2 GB file uploads with a progress bar without touching your server's disk, and a transfer over the cap is refused before upload starts. Do not build yet: download, expiry. ### Phase 2 · Download Build: /t/:slug shows the file list and note, streams each file via a short-lived presigned GET, counts downloads, and refuses after max_downloads. Done when: the recipient downloads with the original filename, the count increments, and the limit is enforced. ### Phase 3 · Expiry and passwords Build: expiry (default 7 days), a nightly job deleting expired objects and rows, and an optional password checked server-side with a constant-time compare and a rate limit. Done when: an expired link returns a clear page, the objects are gone from the bucket, and a wrong password is rate limited. ### Phase 4 · Encryption Build: optional client-side encryption with the Web Crypto API, the key carried in the URL fragment so the server never sees it; decryption in the recipient's browser. Done when: the object in the bucket is unreadable and the recipient's download is the original file. ### Phase 5 · Deploy Build: bucket lifecycle rules as a backstop, /healthz, a systemd unit, the README. Done when: a stranger receives and downloads a file using only the link. ### Out of scope (and why) - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. ### README must contain - The bucket cost model and the size cap. - The fragment-key design, in three sentences. ===== AGENTS.md ===== # Agent instructions · WeTransfer indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Cloudflare R2 or Backblaze B2 with presigned URLs, Node 22, node:http and node:sqlite, Web Crypto in the browser, optional. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ===== BUILD_PLAN.md ===== # Build plan · WeTransfer Large-file transfer on a bucket you pay cents for: uploads go straight to object storage with presigned URLs, links expire and count downloads, optional passwords, and optional client-side encryption with the key in the URL fragment so the server never sees it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Upload Presigned PUTs from the browser with progress; multipart over 100 MB; nothing touches your disk. ### Steps 1. Tables and presign endpoint transfers (id long random slug, created_at, expires_at, downloads, max_downloads, password_hash, note), files (id, transfer_id, key, name, size, content_type). Keys random, never the filename. ```sh mkdir send && cd send && git init && npm init -y && npm pkg set type=module && npm install @aws-sdk/client-s3@3 @aws-sdk/s3-request-presigner@3 mkdir -p data && cp .env.example .env ``` 2. The upload page with progress and multipart for large files ### Done when - [ ] A 2 GB file uploads with progress without touching the server's disk - [ ] A transfer over the cap is refused before upload starts ## Phase 2 · Download A file list, short-lived presigned GETs, counted downloads, a limit. ### Steps 1. /t/:slug with the list and presigned GET per file with the original filename 2. Count downloads and refuse after max_downloads ### Done when - [ ] The recipient downloads with the original filename - [ ] The count increments - [ ] The limit is enforced ## Phase 3 · Expiry and passwords Expired links explain themselves; objects are deleted; passwords are rate limited. ### Steps 1. Default 7-day expiry; nightly job deleting objects and rows 2. Optional password with constant-time compare and a rate limit ### Done when - [ ] An expired link returns a clear page - [ ] Objects are gone from the bucket - [ ] A wrong password is rate limited ## Phase 4 · Encryption Key in the fragment; server never sees it. ### Steps 1. Encrypt in the browser with Web Crypto before upload; key in the URL fragment 2. Decrypt in the recipient's browser ### Done when - [ ] The bucket object is unreadable - [ ] The recipient's download is the original file ## Phase 5 · Deploy Lifecycle rules as a backstop, healthz, service, README. ### Steps 1. Bucket lifecycle rules, /healthz, systemd, Caddy 2. README with the cost model and the fragment-key design Files: `README.md` ### Done when - [ ] A stranger receives and downloads a file using only the link ## Not in this build - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. ## After v1, if you want it - Email the link on request via a provider - Upload from a phone via a share target ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/transfers.db # Required. Bucket endpoint. S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com # Required. Bucket name. S3_BUCKET=transfers # Required · secret. Bucket key. S3_ACCESS_KEY_ID=... # Required · secret. Bucket secret. S3_SECRET_ACCESS_KEY=... # Optional. Total size cap per transfer. MAX_TRANSFER_GB=20 # Required. Public base URL. SITE_URL=https://send.yourdomain.com
You are building a lean indie version of WeTransfer. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # WeTransfer · indie build Large-file transfer on a bucket you pay cents for: uploads go straight to object storage with presigned URLs, links expire and count downloads, optional passwords, and optional client-side encryption with the key in the URL fragment so the server never sees it. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Storage | Cloudflare R2 or Backblaze B2 with presigned URLs | files never pass through your server | | Runtime | Node 22, node:http and node:sqlite | link records and the page | | Encryption | Web Crypto in the browser, optional | privacy WeTransfer never had | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **An S3-compatible bucket with access keys and CORS** · cents to a few dollars a month - Why: Direct browser uploads need CORS on the bucket. - Get it: R2: create bucket, API token, and set CORS to allow PUT from your domain. B2: similar under bucket settings. - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. The page and link table need a small always-on process. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **A domain or subdomain** (optional) · roughly $10 a year, or free on an existing domain - Why: send.yourdomain.com - Get it: Register at Cloudflare Registrar, Porkbun or Namecheap, or use a subdomain of one you already own. You add one DNS record in the deploy phase. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir send && cd send && git init && npm init -y && npm pkg set type=module && npm install @aws-sdk/client-s3@3 @aws-sdk/s3-request-presigner@3 mkdir -p data && cp .env.example .env ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. - bandwidth and speed on their CDN worldwide - the brand a recipient recognises - email delivery of the link and download receipts - the mobile apps and Paste If one of those is essential to you, that is the reason to keep paying for WeTransfer, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · WeTransfer The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a large-file transfer service like WeTransfer for my own sends. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. S3-compatible storage (Cloudflare R2 or Backblaze B2) with presigned URLs so files never pass through your server. Vanilla JS for the upload page. ### Data model (create this before Phase 1) - transfers: id (long random slug), created_at, expires_at, downloads, max_downloads, password_hash (nullable), note - files: id, transfer_id, key, name, size, content_type Object keys are random, never the original filename. ### Phase 1 · Upload Build: the page requests a presigned PUT per file and uploads directly to the bucket with progress; multipart for files over 100 MB; the server records the transfer and files. Cap total size from .env. Done when: a 2 GB file uploads with a progress bar without touching your server's disk, and a transfer over the cap is refused before upload starts. Do not build yet: download, expiry. ### Phase 2 · Download Build: /t/:slug shows the file list and note, streams each file via a short-lived presigned GET, counts downloads, and refuses after max_downloads. Done when: the recipient downloads with the original filename, the count increments, and the limit is enforced. ### Phase 3 · Expiry and passwords Build: expiry (default 7 days), a nightly job deleting expired objects and rows, and an optional password checked server-side with a constant-time compare and a rate limit. Done when: an expired link returns a clear page, the objects are gone from the bucket, and a wrong password is rate limited. ### Phase 4 · Encryption Build: optional client-side encryption with the Web Crypto API, the key carried in the URL fragment so the server never sees it; decryption in the recipient's browser. Done when: the object in the bucket is unreadable and the recipient's download is the original file. ### Phase 5 · Deploy Build: bucket lifecycle rules as a backstop, /healthz, a systemd unit, the README. Done when: a stranger receives and downloads a file using only the link. ### Out of scope (and why) - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. ### README must contain - The bucket cost model and the size cap. - The fragment-key design, in three sentences. ===== AGENTS.md ===== # Agent instructions · WeTransfer indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Cloudflare R2 or Backblaze B2 with presigned URLs, Node 22, node:http and node:sqlite, Web Crypto in the browser, optional. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ===== BUILD_PLAN.md ===== # Build plan · WeTransfer Large-file transfer on a bucket you pay cents for: uploads go straight to object storage with presigned URLs, links expire and count downloads, optional passwords, and optional client-side encryption with the key in the URL fragment so the server never sees it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Upload Presigned PUTs from the browser with progress; multipart over 100 MB; nothing touches your disk. ### Steps 1. Tables and presign endpoint transfers (id long random slug, created_at, expires_at, downloads, max_downloads, password_hash, note), files (id, transfer_id, key, name, size, content_type). Keys random, never the filename. ```sh mkdir send && cd send && git init && npm init -y && npm pkg set type=module && npm install @aws-sdk/client-s3@3 @aws-sdk/s3-request-presigner@3 mkdir -p data && cp .env.example .env ``` 2. The upload page with progress and multipart for large files ### Done when - [ ] A 2 GB file uploads with progress without touching the server's disk - [ ] A transfer over the cap is refused before upload starts ## Phase 2 · Download A file list, short-lived presigned GETs, counted downloads, a limit. ### Steps 1. /t/:slug with the list and presigned GET per file with the original filename 2. Count downloads and refuse after max_downloads ### Done when - [ ] The recipient downloads with the original filename - [ ] The count increments - [ ] The limit is enforced ## Phase 3 · Expiry and passwords Expired links explain themselves; objects are deleted; passwords are rate limited. ### Steps 1. Default 7-day expiry; nightly job deleting objects and rows 2. Optional password with constant-time compare and a rate limit ### Done when - [ ] An expired link returns a clear page - [ ] Objects are gone from the bucket - [ ] A wrong password is rate limited ## Phase 4 · Encryption Key in the fragment; server never sees it. ### Steps 1. Encrypt in the browser with Web Crypto before upload; key in the URL fragment 2. Decrypt in the recipient's browser ### Done when - [ ] The bucket object is unreadable - [ ] The recipient's download is the original file ## Phase 5 · Deploy Lifecycle rules as a backstop, healthz, service, README. ### Steps 1. Bucket lifecycle rules, /healthz, systemd, Caddy 2. README with the cost model and the fragment-key design Files: `README.md` ### Done when - [ ] A stranger receives and downloads a file using only the link ## Not in this build - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. ## After v1, if you want it - Email the link on request via a provider - Upload from a phone via a share target ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/transfers.db # Required. Bucket endpoint. S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com # Required. Bucket name. S3_BUCKET=transfers # Required · secret. Bucket key. S3_ACCESS_KEY_ID=... # Required · secret. Bucket secret. S3_SECRET_ACCESS_KEY=... # Optional. Total size cap per transfer. MAX_TRANSFER_GB=20 # Required. Public base URL. SITE_URL=https://send.yourdomain.com
You are building a production product version of WeTransfer. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # WeTransfer · product brief ## Problem Upload to object storage, mint a link, expire it. With client-side encryption you get the privacy WeTransfer never had. Their moat is bandwidth at scale and the brand a client trusts when a link arrives; for your own sends, a $5 bucket and a page does it. ## Product outcome File transfer for a small team on your domain, cheaper and more private than the service it replaces. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - S3-compatible storage (R2 or B2) - a small host for the page and link table ## Explicit non-goals for v1 - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. - bandwidth and speed on their CDN worldwide - the brand a recipient recognises - email delivery of the link and download receipts - the mobile apps and Paste ## Success criteria - Presigned upload verified without server disk use - Expiry deletes objects - Encryption round-trip verified ===== BRIEF.md ===== # Build brief · WeTransfer The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a large-file transfer service like WeTransfer for my own sends. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. S3-compatible storage (Cloudflare R2 or Backblaze B2) with presigned URLs so files never pass through your server. Vanilla JS for the upload page. ### Data model (create this before Phase 1) - transfers: id (long random slug), created_at, expires_at, downloads, max_downloads, password_hash (nullable), note - files: id, transfer_id, key, name, size, content_type Object keys are random, never the original filename. ### Phase 1 · Upload Build: the page requests a presigned PUT per file and uploads directly to the bucket with progress; multipart for files over 100 MB; the server records the transfer and files. Cap total size from .env. Done when: a 2 GB file uploads with a progress bar without touching your server's disk, and a transfer over the cap is refused before upload starts. Do not build yet: download, expiry. ### Phase 2 · Download Build: /t/:slug shows the file list and note, streams each file via a short-lived presigned GET, counts downloads, and refuses after max_downloads. Done when: the recipient downloads with the original filename, the count increments, and the limit is enforced. ### Phase 3 · Expiry and passwords Build: expiry (default 7 days), a nightly job deleting expired objects and rows, and an optional password checked server-side with a constant-time compare and a rate limit. Done when: an expired link returns a clear page, the objects are gone from the bucket, and a wrong password is rate limited. ### Phase 4 · Encryption Build: optional client-side encryption with the Web Crypto API, the key carried in the URL fragment so the server never sees it; decryption in the recipient's browser. Done when: the object in the bucket is unreadable and the recipient's download is the original file. ### Phase 5 · Deploy Build: bucket lifecycle rules as a backstop, /healthz, a systemd unit, the README. Done when: a stranger receives and downloads a file using only the link. ### Out of scope (and why) - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. ### README must contain - The bucket cost model and the size cap. - The fragment-key design, in three sentences. ===== ARCHITECTURE.md ===== # Architecture · WeTransfer ## Stack | Part | Choice | Why | | --- | --- | --- | | Storage | Cloudflare R2 or Backblaze B2 with presigned URLs | files never pass through your server | | Runtime | Node 22, node:http and node:sqlite | link records and the page | | Encryption | Web Crypto in the browser, optional | privacy WeTransfer never had | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Presign | upload and download URLs | Any S3-compatible storage | | Links | transfers, expiry, passwords | The core | | Crypto | browser encryption | Optional | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Any free port. - `DATABASE_PATH` · required · SQLite file. - `S3_ENDPOINT` · required · Bucket endpoint. - `S3_BUCKET` · required · Bucket name. - `S3_ACCESS_KEY_ID` · required, secret · Bucket key. - `S3_SECRET_ACCESS_KEY` · required, secret · Bucket secret. - `MAX_TRANSFER_GB` · optional · Total size cap per transfer. - `SITE_URL` · required · Public base URL. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · WeTransfer product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Cloudflare R2 or Backblaze B2 with presigned URLs, Node 22, node:http and node:sqlite, Web Crypto in the browser, optional. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ===== MILESTONES.md ===== # Delivery milestones · WeTransfer Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Upload Presigned PUTs from the browser with progress; multipart over 100 MB; nothing touches your disk. ### Steps 1. Tables and presign endpoint transfers (id long random slug, created_at, expires_at, downloads, max_downloads, password_hash, note), files (id, transfer_id, key, name, size, content_type). Keys random, never the filename. ```sh mkdir send && cd send && git init && npm init -y && npm pkg set type=module && npm install @aws-sdk/client-s3@3 @aws-sdk/s3-request-presigner@3 mkdir -p data && cp .env.example .env ``` 2. The upload page with progress and multipart for large files ### Done when - [ ] A 2 GB file uploads with progress without touching the server's disk - [ ] A transfer over the cap is refused before upload starts ## M2 · Download A file list, short-lived presigned GETs, counted downloads, a limit. ### Steps 1. /t/:slug with the list and presigned GET per file with the original filename 2. Count downloads and refuse after max_downloads ### Done when - [ ] The recipient downloads with the original filename - [ ] The count increments - [ ] The limit is enforced ## M3 · Expiry and passwords Expired links explain themselves; objects are deleted; passwords are rate limited. ### Steps 1. Default 7-day expiry; nightly job deleting objects and rows 2. Optional password with constant-time compare and a rate limit ### Done when - [ ] An expired link returns a clear page - [ ] Objects are gone from the bucket - [ ] A wrong password is rate limited ## M4 · Encryption Key in the fragment; server never sees it. ### Steps 1. Encrypt in the browser with Web Crypto before upload; key in the URL fragment 2. Decrypt in the recipient's browser ### Done when - [ ] The bucket object is unreadable - [ ] The recipient's download is the original file ## M5 · Deploy Lifecycle rules as a backstop, healthz, service, README. ### Steps 1. Bucket lifecycle rules, /healthz, systemd, Caddy 2. README with the cost model and the fragment-key design Files: `README.md` ### Done when - [ ] A stranger receives and downloads a file using only the link ## M6 · Operate it like a product (production only) Only for the product-builder path: know when the transfer page is down, never lose the database, and keep the server patched. ### Steps 1. Add a /healthz endpoint and an external uptime check against it Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices. 2. Write structured request logs and rotate them One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight. 3. Back the SQLite file up off the machine nightly and test a restore SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it. ```sh sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/ ``` 4. Lock the box down Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure. ### Done when - [ ] Stopping the service triggers an uptime alert within a few minutes - [ ] A restore from last night's backup contains yesterday's data - [ ] A port scan from another machine shows only 22, 80 and 443 ===== OPERATIONS.md ===== # Operations · WeTransfer ## Backup SQLite nightly; objects are transient by design. ## Restore Copy back; expired objects are gone regardless. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Uptime and bucket size. ## Incident checklist Rotate bucket keys if leaked; presigned URLs are short-lived. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Presigned upload verified without server disk use - [ ] Expiry deletes objects - [ ] Encryption round-trip verified ## Launch constraint Do not market omitted WeTransfer capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/transfers.db # Required. Bucket endpoint. S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com # Required. Bucket name. S3_BUCKET=transfers # Required · secret. Bucket key. S3_ACCESS_KEY_ID=... # Required · secret. Bucket secret. S3_SECRET_ACCESS_KEY=... # Optional. Total size cap per transfer. MAX_TRANSFER_GB=20 # Required. Public base URL. SITE_URL=https://send.yourdomain.com
# WeTransfer · indie build Large-file transfer on a bucket you pay cents for: uploads go straight to object storage with presigned URLs, links expire and count downloads, optional passwords, and optional client-side encryption with the key in the URL fragment so the server never sees it. Estimated effort: **one sitting**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Storage | Cloudflare R2 or Backblaze B2 with presigned URLs | files never pass through your server | | Runtime | Node 22, node:http and node:sqlite | link records and the page | | Encryption | Web Crypto in the browser, optional | privacy WeTransfer never had | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **An S3-compatible bucket with access keys and CORS** · cents to a few dollars a month - Why: Direct browser uploads need CORS on the bucket. - Get it: R2: create bucket, API token, and set CORS to allow PUT from your domain. B2: similar under bucket settings. - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. The page and link table need a small always-on process. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **A domain or subdomain** (optional) · roughly $10 a year, or free on an existing domain - Why: send.yourdomain.com - Get it: Register at Cloudflare Registrar, Porkbun or Namecheap, or use a subdomain of one you already own. You add one DNS record in the deploy phase. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir send && cd send && git init && npm init -y && npm pkg set type=module && npm install @aws-sdk/client-s3@3 @aws-sdk/s3-request-presigner@3 mkdir -p data && cp .env.example .env ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. - bandwidth and speed on their CDN worldwide - the brand a recipient recognises - email delivery of the link and download receipts - the mobile apps and Paste If one of those is essential to you, that is the reason to keep paying for WeTransfer, and the README should say so rather than pretend.
# Build brief · WeTransfer The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a large-file transfer service like WeTransfer for my own sends. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. S3-compatible storage (Cloudflare R2 or Backblaze B2) with presigned URLs so files never pass through your server. Vanilla JS for the upload page. ### Data model (create this before Phase 1) - transfers: id (long random slug), created_at, expires_at, downloads, max_downloads, password_hash (nullable), note - files: id, transfer_id, key, name, size, content_type Object keys are random, never the original filename. ### Phase 1 · Upload Build: the page requests a presigned PUT per file and uploads directly to the bucket with progress; multipart for files over 100 MB; the server records the transfer and files. Cap total size from .env. Done when: a 2 GB file uploads with a progress bar without touching your server's disk, and a transfer over the cap is refused before upload starts. Do not build yet: download, expiry. ### Phase 2 · Download Build: /t/:slug shows the file list and note, streams each file via a short-lived presigned GET, counts downloads, and refuses after max_downloads. Done when: the recipient downloads with the original filename, the count increments, and the limit is enforced. ### Phase 3 · Expiry and passwords Build: expiry (default 7 days), a nightly job deleting expired objects and rows, and an optional password checked server-side with a constant-time compare and a rate limit. Done when: an expired link returns a clear page, the objects are gone from the bucket, and a wrong password is rate limited. ### Phase 4 · Encryption Build: optional client-side encryption with the Web Crypto API, the key carried in the URL fragment so the server never sees it; decryption in the recipient's browser. Done when: the object in the bucket is unreadable and the recipient's download is the original file. ### Phase 5 · Deploy Build: bucket lifecycle rules as a backstop, /healthz, a systemd unit, the README. Done when: a stranger receives and downloads a file using only the link. ### Out of scope (and why) - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. ### README must contain - The bucket cost model and the size cap. - The fragment-key design, in three sentences.
# Agent instructions · WeTransfer indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Cloudflare R2 or Backblaze B2 with presigned URLs, Node 22, node:http and node:sqlite, Web Crypto in the browser, optional. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
# Build plan · WeTransfer Large-file transfer on a bucket you pay cents for: uploads go straight to object storage with presigned URLs, links expire and count downloads, optional passwords, and optional client-side encryption with the key in the URL fragment so the server never sees it. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Upload Presigned PUTs from the browser with progress; multipart over 100 MB; nothing touches your disk. ### Steps 1. Tables and presign endpoint transfers (id long random slug, created_at, expires_at, downloads, max_downloads, password_hash, note), files (id, transfer_id, key, name, size, content_type). Keys random, never the filename. ```sh mkdir send && cd send && git init && npm init -y && npm pkg set type=module && npm install @aws-sdk/client-s3@3 @aws-sdk/s3-request-presigner@3 mkdir -p data && cp .env.example .env ``` 2. The upload page with progress and multipart for large files ### Done when - [ ] A 2 GB file uploads with progress without touching the server's disk - [ ] A transfer over the cap is refused before upload starts ## Phase 2 · Download A file list, short-lived presigned GETs, counted downloads, a limit. ### Steps 1. /t/:slug with the list and presigned GET per file with the original filename 2. Count downloads and refuse after max_downloads ### Done when - [ ] The recipient downloads with the original filename - [ ] The count increments - [ ] The limit is enforced ## Phase 3 · Expiry and passwords Expired links explain themselves; objects are deleted; passwords are rate limited. ### Steps 1. Default 7-day expiry; nightly job deleting objects and rows 2. Optional password with constant-time compare and a rate limit ### Done when - [ ] An expired link returns a clear page - [ ] Objects are gone from the bucket - [ ] A wrong password is rate limited ## Phase 4 · Encryption Key in the fragment; server never sees it. ### Steps 1. Encrypt in the browser with Web Crypto before upload; key in the URL fragment 2. Decrypt in the recipient's browser ### Done when - [ ] The bucket object is unreadable - [ ] The recipient's download is the original file ## Phase 5 · Deploy Lifecycle rules as a backstop, healthz, service, README. ### Steps 1. Bucket lifecycle rules, /healthz, systemd, Caddy 2. README with the cost model and the fragment-key design Files: `README.md` ### Done when - [ ] A stranger receives and downloads a file using only the link ## Not in this build - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. ## After v1, if you want it - Email the link on request via a provider - Upload from a phone via a share target
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/transfers.db # Required. Bucket endpoint. S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com # Required. Bucket name. S3_BUCKET=transfers # Required · secret. Bucket key. S3_ACCESS_KEY_ID=... # Required · secret. Bucket secret. S3_SECRET_ACCESS_KEY=... # Optional. Total size cap per transfer. MAX_TRANSFER_GB=20 # Required. Public base URL. SITE_URL=https://send.yourdomain.com
# WeTransfer · product brief ## Problem Upload to object storage, mint a link, expire it. With client-side encryption you get the privacy WeTransfer never had. Their moat is bandwidth at scale and the brand a client trusts when a link arrives; for your own sends, a $5 bucket and a page does it. ## Product outcome File transfer for a small team on your domain, cheaper and more private than the service it replaces. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - S3-compatible storage (R2 or B2) - a small host for the page and link table ## Explicit non-goals for v1 - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. - bandwidth and speed on their CDN worldwide - the brand a recipient recognises - email delivery of the link and download receipts - the mobile apps and Paste ## Success criteria - Presigned upload verified without server disk use - Expiry deletes objects - Encryption round-trip verified
# Build brief · WeTransfer The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a large-file transfer service like WeTransfer for my own sends. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. S3-compatible storage (Cloudflare R2 or Backblaze B2) with presigned URLs so files never pass through your server. Vanilla JS for the upload page. ### Data model (create this before Phase 1) - transfers: id (long random slug), created_at, expires_at, downloads, max_downloads, password_hash (nullable), note - files: id, transfer_id, key, name, size, content_type Object keys are random, never the original filename. ### Phase 1 · Upload Build: the page requests a presigned PUT per file and uploads directly to the bucket with progress; multipart for files over 100 MB; the server records the transfer and files. Cap total size from .env. Done when: a 2 GB file uploads with a progress bar without touching your server's disk, and a transfer over the cap is refused before upload starts. Do not build yet: download, expiry. ### Phase 2 · Download Build: /t/:slug shows the file list and note, streams each file via a short-lived presigned GET, counts downloads, and refuses after max_downloads. Done when: the recipient downloads with the original filename, the count increments, and the limit is enforced. ### Phase 3 · Expiry and passwords Build: expiry (default 7 days), a nightly job deleting expired objects and rows, and an optional password checked server-side with a constant-time compare and a rate limit. Done when: an expired link returns a clear page, the objects are gone from the bucket, and a wrong password is rate limited. ### Phase 4 · Encryption Build: optional client-side encryption with the Web Crypto API, the key carried in the URL fragment so the server never sees it; decryption in the recipient's browser. Done when: the object in the bucket is unreadable and the recipient's download is the original file. ### Phase 5 · Deploy Build: bucket lifecycle rules as a backstop, /healthz, a systemd unit, the README. Done when: a stranger receives and downloads a file using only the link. ### Out of scope (and why) - Worldwide CDN bandwidth, the brand, email delivery and receipts, the apps. ### README must contain - The bucket cost model and the size cap. - The fragment-key design, in three sentences.
# Architecture · WeTransfer ## Stack | Part | Choice | Why | | --- | --- | --- | | Storage | Cloudflare R2 or Backblaze B2 with presigned URLs | files never pass through your server | | Runtime | Node 22, node:http and node:sqlite | link records and the page | | Encryption | Web Crypto in the browser, optional | privacy WeTransfer never had | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Presign | upload and download URLs | Any S3-compatible storage | | Links | transfers, expiry, passwords | The core | | Crypto | browser encryption | Optional | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Any free port. - `DATABASE_PATH` · required · SQLite file. - `S3_ENDPOINT` · required · Bucket endpoint. - `S3_BUCKET` · required · Bucket name. - `S3_ACCESS_KEY_ID` · required, secret · Bucket key. - `S3_SECRET_ACCESS_KEY` · required, secret · Bucket secret. - `MAX_TRANSFER_GB` · optional · Total size cap per transfer. - `SITE_URL` · required · Public base URL. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · WeTransfer product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Cloudflare R2 or Backblaze B2 with presigned URLs, Node 22, node:http and node:sqlite, Web Crypto in the browser, optional. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone.
# Delivery milestones · WeTransfer Estimated effort: **one sitting** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Upload Presigned PUTs from the browser with progress; multipart over 100 MB; nothing touches your disk. ### Steps 1. Tables and presign endpoint transfers (id long random slug, created_at, expires_at, downloads, max_downloads, password_hash, note), files (id, transfer_id, key, name, size, content_type). Keys random, never the filename. ```sh mkdir send && cd send && git init && npm init -y && npm pkg set type=module && npm install @aws-sdk/client-s3@3 @aws-sdk/s3-request-presigner@3 mkdir -p data && cp .env.example .env ``` 2. The upload page with progress and multipart for large files ### Done when - [ ] A 2 GB file uploads with progress without touching the server's disk - [ ] A transfer over the cap is refused before upload starts ## M2 · Download A file list, short-lived presigned GETs, counted downloads, a limit. ### Steps 1. /t/:slug with the list and presigned GET per file with the original filename 2. Count downloads and refuse after max_downloads ### Done when - [ ] The recipient downloads with the original filename - [ ] The count increments - [ ] The limit is enforced ## M3 · Expiry and passwords Expired links explain themselves; objects are deleted; passwords are rate limited. ### Steps 1. Default 7-day expiry; nightly job deleting objects and rows 2. Optional password with constant-time compare and a rate limit ### Done when - [ ] An expired link returns a clear page - [ ] Objects are gone from the bucket - [ ] A wrong password is rate limited ## M4 · Encryption Key in the fragment; server never sees it. ### Steps 1. Encrypt in the browser with Web Crypto before upload; key in the URL fragment 2. Decrypt in the recipient's browser ### Done when - [ ] The bucket object is unreadable - [ ] The recipient's download is the original file ## M5 · Deploy Lifecycle rules as a backstop, healthz, service, README. ### Steps 1. Bucket lifecycle rules, /healthz, systemd, Caddy 2. README with the cost model and the fragment-key design Files: `README.md` ### Done when - [ ] A stranger receives and downloads a file using only the link ## M6 · Operate it like a product (production only) Only for the product-builder path: know when the transfer page is down, never lose the database, and keep the server patched. ### Steps 1. Add a /healthz endpoint and an external uptime check against it Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices. 2. Write structured request logs and rotate them One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight. 3. Back the SQLite file up off the machine nightly and test a restore SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it. ```sh sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/ ``` 4. Lock the box down Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure. ### Done when - [ ] Stopping the service triggers an uptime alert within a few minutes - [ ] A restore from last night's backup contains yesterday's data - [ ] A port scan from another machine shows only 22, 80 and 443
# Operations · WeTransfer ## Backup SQLite nightly; objects are transient by design. ## Restore Copy back; expired objects are gone regardless. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Uptime and bucket size. ## Incident checklist Rotate bucket keys if leaked; presigned URLs are short-lived. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Presigned upload verified without server disk use - [ ] Expiry deletes objects - [ ] Encryption round-trip verified ## Launch constraint Do not market omitted WeTransfer capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/transfers.db # Required. Bucket endpoint. S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com # Required. Bucket name. S3_BUCKET=transfers # Required · secret. Bucket key. S3_ACCESS_KEY_ID=... # Required · secret. Bucket secret. S3_SECRET_ACCESS_KEY=... # Optional. Total size cap per transfer. MAX_TRANSFER_GB=20 # Required. Public base URL. SITE_URL=https://send.yourdomain.com
$ choose a build depth, inspect the files, then open the complete pack in your agent
Because a WeTransfer link is something a client opens without asking what it is, and their bandwidth is not on your bill.
xbandwidth and speed on their CDN worldwide
xthe brand a recipient recognises
xemail delivery of the link and download receipts
xthe mobile apps and Paste
WeTransfer pricing
starter$8/mo · monthly flat · $96/yr
free tierThe free plan allows 10 transfers or 3 GB in any 30-day window.
verified 2026-09-04 · source ↗
Is WeTransfer free?
The free plan allows 10 transfers or 3 GB in any 30-day window. Paid is Starter at $8/mo (checked 2026-09-04).
Vibecode WeTransfer
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal WeTransfer replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does WeTransfer cost?
WeTransfer costs about $8/month (Starter, checked 2026-09-04), which is $96 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing WeTransfer?
Honestly: bandwidth and speed on their CDN worldwide; the brand a recipient recognises; email delivery of the link and download receipts; the mobile apps and Paste. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to WeTransfer?
Yes: Send (encrypted file sharing, the continued Firefox Send). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.