Post Transfer
Host your own file transfers on Windows, macOS or Linux. A browser portal plus a desktop client — you choose where your files live: an internal disk, an attached drive, or a mounted network share.
Downloads
I built this to send dailies and deliverables on my own jobs, and I use it daily. I'm sharing it as it is — you're free to use it however works for you. This is a source install — see below for setup on your OS.
Did you like this app? If it saved you time or money, you can buy me a coffee.
Buy me a coffeeWhat you need
Post Transfer combines a browser portal with a desktop transfer client. SFTPGo manages accounts, storage access and sharing permissions; the portal supplies the interface; Caddy supplies HTTPS for public access. The computer hosting the server must stay powered on and connected while others transfer files.
| Goal | Install |
|---|---|
| Connect to a server someone else operates | Desktop client only — see "Run or build the desktop client" |
| Use your own files on one computer | SFTPGo and the web portal |
| Share files with people outside your network | Also configure a domain and HTTPS |
| Keep the portal running after closing the terminal | Set up the service for your OS |
Download source and prerequisites
Extract the package so that server, desktop and config sit directly inside the installation folder: C:\PostTransfer on Windows, ~/PostTransfer on macOS, /opt/post-transfer on Linux. Use Node.js 22 or a newer supported LTS — this source was checked with Node.js 22 and the SFTPGo 2.7.5 API.
Windows
- Download the Windows SFTPGo installer from the official releases (Assets, x86-64/x64) and run it as administrator, keeping the Windows service option.
- Install Node.js LTS from the official download page, keeping npm and the PATH option enabled.
- Extract the source to
C:\PostTransfer. ConfirmC:\PostTransfer\server\server.jsexists, with no extra nested folder. - Only if you need public HTTPS: install Caddy from its official page and place
caddy.exeinC:\PostTransfer.
Then, in a new PowerShell window:
npm.cmd --version
Get-Service SFTPGo
Use npm.cmd to avoid PowerShell execution-policy errors. Run service/firewall commands as Administrator.
macOS — with Homebrew:
export PATH="$(brew --prefix node@22)/bin:$PATH"
node --version
npm --version
brew info sftpgo
Repeat the export PATH line in each new terminal used for installation.
Linux (Ubuntu/Debian) — install Node.js 22+ via the official instructions, then, in the folder with the downloaded SFTPGo .deb:
sudo systemctl enable --now sftpgo
node --version
npm --version
command -v node
Other distributions: use the SFTPGo installation instructions. Public HTTPS also needs Caddy, via its official package instructions.
Configure the SFTPGo backend
| Component | Listen address | Port |
|---|---|---|
| SFTPGo admin and API | 127.0.0.1 | 8080 |
| SFTPGo WebDAV | 127.0.0.1 | 10081 |
| Post Transfer portal | 127.0.0.1 | 18090 |
| Caddy public site (if enabled) | Public interface | 80 / 443 |
| SFTP desktop connections (if enabled) | Your chosen interface | 2022 |
Keep the backend HTTP ports private. Use 10081, not 10080 — Node's Fetch implementation blocks port 10080.
Back up the active SFTPGo configuration before editing it. Typical paths: Windows C:\ProgramData\SFTPGo\sftpgo.json; macOS Homebrew $(brew --prefix)/etc/sftpgo/sftpgo.json; Linux package /etc/sftpgo/sftpgo.json.
Copy-Item -LiteralPath $p -Destination "$p.backup-$(Get-Date -Format yyyyMMdd-HHmmss)"
notepad.exe $p
On macOS: sudo nano "$(brew --prefix)/etc/sftpgo/sftpgo.json". On Linux: sudo nano /etc/sftpgo/sftpgo.json (back up first).
Edit the existing file, don't replace it. In the first httpd.bindings object: port 8080, address 127.0.0.1, enable_https false — keep enable_web_admin, enable_web_client and enable_rest_api enabled. In the first webdavd.bindings object: port 10081, address 127.0.0.1, enable_https false. For desktop SFTP, set the first sftpd.bindings port to 2022 (address 127.0.0.1 for same-computer use, or 0.0.0.0 for remote, with firewall access limited appropriately). See the SFTPGo configuration reference.
Restart SFTPGo after editing (this interrupts active transfers):
curl.exe -I http://127.0.0.1:8080/web/admin/login
curl.exe -I http://127.0.0.1:10081/
macOS: sudo brew services restart sftpgo. Linux: sudo systemctl restart sftpgo. Then run the two curl checks above with plain curl. A WebDAV 401 Unauthorized without credentials is expected — it confirms the endpoint is listening. "Connection refused" means the service or binding needs attention.
Create accounts and select storage
On the server, open http://127.0.0.1:8080/web/admin/, complete the initial administrator setup if prompted, and save that password securely. Then, under Users, add a user:
- Set a username, password and enabled status.
- Choose Local filesystem storage and an existing home directory (see examples below).
- Add permissions for virtual path
/— list and download by default; add upload/create-directories when needed, rename/delete only when intended. - Allow HTTP/Web Client and WebDAV, plus SFTP if using the desktop app.
- Save, then test the account at
http://127.0.0.1:8080/web/client/before installing the portal.
| Storage | Example home directory |
|---|---|
| Windows internal/attached disk | D:\TransferData\ClientA |
| Windows network share | \\NAS\Projects\ClientA |
| macOS disk | /Users/Shared/TransferData/ClientA |
| macOS mounted drive | /Volumes/Projects/ClientA |
| Linux disk/mounted share | /srv/transfer/ClientA |
The SFTPGo service account needs read (plus write for uploads) access to that directory and its parents, e.g. on a fresh Linux install: sudo install -d -o sftpgo -g sftpgo -m 0750 /srv/transfer/ClientA. On Windows network shares, use a dedicated service account and a UNC path rather than a desktop-mapped drive. Never put SMB passwords in the portal configuration.
Install the web portal
Windows
Copy-Item .env.example .env
npm.cmd ci --omit=dev
node --env-file=.env server.js
macOS
cp .env.example .env
npm ci --omit=dev && node --env-file=.env server.js
Linux — extract into /opt/post-transfer, owned by your installation account while installing, then:
cp .env.example .env
npm ci --omit=dev && node --env-file=.env server.js
The default .env:
PORT=18090
SFTPGO_URL=http://127.0.0.1:8080
WEBDAV_URL=http://127.0.0.1:10081
PUBLIC_URL=http://localhost:18090
COOKIE_SECURE=false
Only copy .env.example on the first install — preserve .env during updates. Leave the terminal open while testing, and open http://localhost:18090/: /studio to create deliveries, /client to upload/download with a transfer account.
Verify the complete transfer path
- Sign in with the user created above.
- Download a small known file and verify its contents.
- Upload a test file (if permitted) and check it lands in the assigned storage folder.
- Create a delivery from
/studio; open its link in a private browser window and test it. - Repeat with a large file and check memory use stays reasonable.
At this stage localhost links only work on the same computer — they can't be sent to external clients yet. That needs public HTTPS (below).
Keep the portal running
Press Ctrl+C in the test terminal before installing a service on the same port — never run two copies against port 18090.
Windows service — download WinSW-x64.exe v2.12.0 from its release page into C:\PostTransfer, then:
Copy-Item WinSW-x64.exe PostTransferPortal.exe
Copy-Item config\PostTransferPortal.xml PostTransferPortal.xml
New-Item -ItemType Directory -Path C:\PostTransfer\logs -Force
icacls C:\PostTransfer /grant "*S-1-5-19:(OI)(CI)RX"
icacls C:\PostTransfer\logs /grant "*S-1-5-19:(OI)(CI)M"
.\PostTransferPortal.exe install
Start-Service PostTransferPortal
Get-Service PostTransferPortal
Edit <executable> in the XML first if Node isn't at C:\Program Files\nodejs\node.exe. Logs land in C:\PostTransfer\logs.
macOS login service — survives closing Terminal, starts on your user login (not before):
cp "$HOME/PostTransfer/config/com.joaofariapost.posttransfer.plist" "$HOME/Library/LaunchAgents/"
command -v node
echo "$HOME"
open -e "$HOME/Library/LaunchAgents/com.joaofariapost.posttransfer.plist"
Replace the Node placeholder and every /Users/YOUR_USERNAME with the printed paths (XML needs absolute paths — ~ and $HOME don't expand there). Then:
launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/com.joaofariapost.posttransfer.plist"
curl -I http://127.0.0.1:18090/
Linux systemd service — for a new install:
sudo chown -R root:posttransfer /opt/post-transfer/server
sudo chmod -R g+rX,o-rwx /opt/post-transfer/server
sudo chmod 640 /opt/post-transfer/server/.env
sudo cp /opt/post-transfer/config/post-transfer.service /etc/systemd/system/
command -v node
sudo nano /etc/systemd/system/post-transfer.service
Set ExecStart to the real Node path if not /usr/bin/node, then:
sudo systemctl enable --now post-transfer
sudo systemctl status post-transfer --no-pager
curl -I http://127.0.0.1:18090/
Enable public HTTPS access
Use your own subdomain, e.g. transfer.example.com. Point its DNS to your server's public IP, forward TCP 80/443 if behind a router, and confirm no other web server already owns those ports. Edit the portal's .env: PUBLIC_URL=https://transfer.example.com (your real domain) and COOKIE_SECURE=true, keeping HOST=127.0.0.1. Restart the portal service, then configure Caddy using config/Caddyfile.example with your domain — it proxies the portal and the native SFTPGo client/API; admin pages stay local.
Windows HTTPS service — copy the example to C:\PostTransfer\Caddyfile, add this global block at the very top:
storage file_system {
root C:/PostTransfer/caddy-data
}
}
Then, in Administrator PowerShell:
New-Item -ItemType Directory -Path C:\PostTransfer\caddy-data -Force
icacls C:\PostTransfer\caddy-data /inheritance:r /grant:r "*S-1-5-18:(OI)(CI)F" "*S-1-5-32-544:(OI)(CI)F" "*S-1-5-19:(OI)(CI)M"
.\caddy.exe validate --config C:\PostTransfer\Caddyfile --adapter caddyfile
Copy-Item WinSW-x64.exe PostTransferCaddy.exe
Copy-Item config\PostTransferCaddy.xml PostTransferCaddy.xml
.\PostTransferCaddy.exe install
New-NetFirewallRule -DisplayName "Post Transfer HTTPS" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 80,443 -Program C:\PostTransfer\caddy.exe
Restart-Service PostTransferPortal
Start-Service PostTransferCaddy
macOS HTTPS — copy the edited example to $(brew --prefix)/etc/Caddyfile (merge if Caddy already serves other sites):
sudo brew services start caddy
launchctl kickstart -k "gui/$(id -u)/com.joaofariapost.posttransfer"
Linux HTTPS — add the edited site block to /etc/caddy/Caddyfile:
sudo systemctl restart post-transfer
sudo systemctl enable --now caddy
sudo systemctl reload caddy
If UFW is your firewall: sudo ufw allow 80/tcp and sudo ufw allow 443/tcp. Never expose backend ports 8080, 10081 or 18090 publicly.
External check — open the real HTTPS address from a different connection (e.g. a phone on mobile data). Verify login, a file download, an upload if allowed, and a new public delivery link. Any existing link containing localhost needs to be recreated with the public domain.
Run or build the desktop client
The desktop source is under desktop. It can connect to a separately installed SFTPGo server or open an HTTPS public delivery link — you don't need the portal on every client's computer.
Windows
npm.cmd ci
npm.cmd start
macOS
npm ci && npm start
Linux (as a regular desktop user, in the extracted desktop source folder)
For login: server hostname or IPv4 (no https:// prefix), SFTP port, username, password, and the server's verified SHA256 host-key fingerprint — get this from the server administrator through a trusted channel, never from an unverified remote scan.
To build local application packages (build on the matching OS):
Outputs land in desktop/dist. Source archives aren't executable downloads — build before distributing a binary, and sign/notarize with your own publisher credentials where appropriate (none are included here).
Updates, backups and troubleshooting
Back up the portal .env, the SFTPGo configuration/database/host keys, Caddy's configuration/certificate data, and the actual transferred files — the application source is not a backup of your storage. To update: stop the portal service, preserve .env, replace the application source, run npm ci --omit=dev inside server, then restart the service (this invalidates in-memory login sessions).
| Symptom | Check |
|---|---|
| WebDAV 401 in an unauthenticated curl test | Expected — retry through the portal with a transfer user |
| ECONNREFUSED | Backend running, WebDAV enabled, matching port 10081 |
| Fetch reports "bad port" | Replace 10080 with 10081 in both backend and .env |
| 502 Bad Gateway | Portal/backend listener, proxy address, logs |
| Login works but files return 403 | User home permissions, protocol restrictions, forced password change |
| Public link only works on the server | Wrong PUBLIC_URL, DNS, port forwarding, firewall or CGNAT |
| HTTPS fails to start | Port conflict, DNS, certificate reachability, service storage permissions |
| Low speed | Compare local backend vs local portal vs public HTTPS; check disk, CPU, network, user limits |
Contact
Created by João Faria — joaofariapost.com
Questions or feedback? hello@joaofariapost.com