A secure, password-protected, self-hosted file sharing platform.
Browse all disks/drives, navigate folders, preview and download files, and generate QR codes for mobile access — all over HTTP, deployable via PM2 or systemd, and exposable via Cloudflare Tunnel.
- 🔒 Password protection – set access credentials server-side via environment variables
- 💾 Drive/disk listing – shows all real mount points / volumes (Linux, macOS, Windows)
- 📂 Folder navigation – browse any directory with a clean table view
- 👁 File preview – inline preview for images, video, audio, PDF, and text files
- ⬇ File download – direct download with correct MIME type
- 📱 QR code generation – scan on mobile to download any file
- 🌐 Single-server deployment – Express serves the built React app (no separate web server needed)
- ⚡ PM2 / systemd ready – ships with
ecosystem.config.js
git clone https://github.com/PNDLabs/mfaa.git
cd mfaa
npm run install:all # installs server + client dependenciescp .env.example server/.env
nano server/.env # set ACCESS_PASSWORD and JWT_SECRETMinimum required settings in server/.env:
ACCESS_PASSWORD=your-strong-password-here
JWT_SECRET=run-node-e-console.log(require('crypto').randomBytes(64).toString('hex'))To use a bcrypt hash instead of a plain password (recommended):
node -e "const b=require('./server/node_modules/bcryptjs'); b.hash('yourpassword',12).then(console.log)"Then in .env:
# Remove ACCESS_PASSWORD and use this instead:
ACCESS_PASSWORD_HASH=$2a$12$...hash...npm run build# Development (two terminals)
npm run dev:server
npm run dev:client
# Production (single process, serves built frontend)
npm start
# Production with PM2
pm2 start ecosystem.config.js
pm2 save
pm2 startup # to auto-start on boot| Variable | Required | Default | Description |
|---|---|---|---|
ACCESS_PASSWORD |
Yes* | – | Plain-text access password |
ACCESS_PASSWORD_HASH |
Yes* | – | bcrypt hash (overrides ACCESS_PASSWORD) |
JWT_SECRET |
Yes | insecure default | Secret for signing JWT tokens |
TOKEN_EXPIRY |
No | 24h |
Token validity (e.g. 1h, 7d) |
PORT |
No | 3001 |
HTTP port (overridden to 3039 in ecosystem.config.js for PM2 deployments — keep .env and ecosystem.config.js in sync) |
ALLOWED_ORIGINS |
No | * |
Comma-separated CORS origins |
SHOW_HIDDEN |
No | false |
Show hidden files (.dotfiles) |
NODE_ENV |
No | development |
production silences debug output |
*Exactly one of ACCESS_PASSWORD or ACCESS_PASSWORD_HASH must be set.
- Install cloudflared
- Authenticate and create a tunnel:
cloudflared tunnel login cloudflared tunnel create mfaa
- Create
~/.cloudflared/config.yml(or, ifcloudflaredis already installed as a system service, add an ingress entry to the existing/etc/cloudflared/config.ymlinstead of creating a new tunnel):tunnel: <TUNNEL_ID> credentials-file: /home/user/.cloudflared/<TUNNEL_ID>.json ingress: - hostname: files.yourdomain.com service: http://localhost:3039 # must match PORT in .env / ecosystem.config.js - service: http_status:404
- Add to your domain's DNS (cloudflared will guide you), or if reusing an existing tunnel:
cloudflared tunnel route dns <TUNNEL_NAME> files.yourdomain.com - Start the tunnel (or restart it, if reusing an existing systemd-managed tunnel):
cloudflared tunnel run mfaa # or, for an existing service: sudo systemctl restart cloudflared - Update
ALLOWED_ORIGINSinserver/.env:ALLOWED_ORIGINS=https://files.yourdomain.com
Create /etc/systemd/system/mfaa.service:
[Unit]
Description=MFAA File Server
After=network.target
[Service]
Type=simple
User=youruser
WorkingDirectory=/path/to/mfaa
ExecStart=/usr/bin/node server/src/index.js
EnvironmentFile=/path/to/mfaa/server/.env
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now mfaamfaa/
├── server/ # Node.js + Express backend
│ ├── src/
│ │ ├── index.js # Entry point, Express app
│ │ ├── middleware/
│ │ │ └── auth.js # JWT authentication middleware
│ │ └── routes/
│ │ ├── auth.js # POST /api/auth/login
│ │ ├── files.js # GET /api/drives, /api/files, /api/download, /api/preview
│ │ └── qr.js # GET /api/qr
│ └── package.json
├── client/ # React + Vite frontend
│ ├── src/
│ │ ├── App.jsx
│ │ ├── main.jsx
│ │ ├── api/index.js # API client helpers
│ │ ├── components/
│ │ │ ├── Login.jsx
│ │ │ ├── FileBrowser.jsx
│ │ │ ├── FileItem.jsx
│ │ │ ├── Preview.jsx
│ │ │ └── QRModal.jsx
│ │ └── styles/App.css
│ └── package.json
├── ecosystem.config.js # PM2 config
├── .env.example # Environment variable template
└── package.json # Root convenience scripts
- Never expose
ACCESS_PASSWORDorJWT_SECRETin source control — use the.envfile (already in.gitignore) - The server validates and resolves all file paths to prevent directory traversal attacks
- JWT tokens expire (default 24h); set
TOKEN_EXPIRYas appropriate - For production, always set a strong
JWT_SECRET - Consider setting
ALLOWED_ORIGINSto your specific domain when using Cloudflare Tunnel
MIT