Backend API for JTransfer - a secure, end-to-end encrypted file sharing service.
Active development. main is the working trunk and is not yet promoted for general production use.
- File upload and download with presigned URLs
- End-to-end encryption (keys never touch the server)
- Optional password protection for transfers
- Automatic file expiration (1, 6, 12, 24, or 72 hours)
- Magic byte validation for file type verification
- Rate limiting for security
- Bun - JavaScript runtime
- Elysia - Web framework
- Drizzle ORM - Database ORM
- PostgreSQL - Database
- Redis - Rate limiting (optional, falls back to in-memory)
- Cloudflare R2 - Object storage (accessed via presigned URLs)
- Bun v1.0 or higher
- PostgreSQL database
- Redis (recommended for production)
Create a .env file in the root directory:
# Required - Database
DATABASE_URL=postgresql://user:password@localhost:5432/jtransfer
# Required - Cloudflare R2 (object storage)
R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=...
R2_SECRET_ACCESS_KEY=...
R2_BUCKET_NAME=jtransfer
# Optional - Rate limiting (falls back to in-memory if not set)
REDIS_URL=redis://localhost:6379
# Optional - Defaults shown
CORS_ORIGINS=http://localhost:5173
PORT=3000
MAX_FILE_SIZE=1073741824Rate limiting uses Redis when REDIS_URL is set, with automatic fallback to in-memory for development. For production, Redis is recommended because:
- Rate limits persist across server restarts
- Works with multiple API instances (load balancing)
- Automatic cleanup of expired entries
Install Redis on your server or use a managed service like Upstash.
# Install dependencies
bun install
# Run database migrations
bun run db:migrate
# Start development server
bun run devThe API will be available at http://localhost:3000.
Unit tests run in-process; integration tests require a disposable Postgres database (the harness truncates all tables between tests).
# One-time: create a dedicated test database
createdb jtransferdb_test
# Run the full suite
TEST_DATABASE_URL=postgresql://user:password@localhost:5432/jtransferdb_test bun testTEST_DATABASE_URL must differ from DATABASE_URL — the harness refuses to run otherwise. R2 calls are stubbed; no network I/O.
For the magic-link auth flow, run the out-of-process smoke script (requires the dev API running):
bun run dev # in one shell
bun run auth:smoke # in anotherPOST /api/upload/create-transfer- Create a new transferPOST /api/upload/request-upload-url- Request a presigned R2 upload URL for a filePOST /api/upload/complete- Mark a transfer as completePOST /api/upload/abort- Abort an in-progress transfer
POST /api/validate- Validate file type by magic bytes
GET /api/download/transfer/:id- Get transfer metadataPOST /api/download/transfer/:id/verify- Verify password for protected transfersGET /api/download/file/:id/url- Get a presigned download URL for a file
GET /health- Service health check
JTransfer uses a dual-layer security approach:
-
End-to-end encryption: Files are encrypted in the browser before upload. The encryption key is stored in the URL fragment (after
#) and never sent to the server. -
Optional password protection: An additional server-side access control layer. Passwords are hashed using Argon2id.
Built by Jimmy Verburgt. Contact via jimmyverburgt@gmail.com or GitHub.
Copyright © 2024–2026 Jimmy Verburgt.
Source code is licensed under the GNU Affero General Public License v3.0 — see LICENSE for the full text. AGPL-3.0 is a strong copyleft license: anyone who runs a modified version of JTransfer as a network service must make the source of their modifications available to users of that service.
The JTransfer name, logo, and visual identity are trademarks and are not licensed under AGPL — see TRADEMARK.md for what you can and cannot do with the brand. Forks must rename and re-brand before being run as a service.