Self-hosted file sharing for teams. Authenticated users send files to external recipients or collect uploads via inbound file requests. Public links are password-protected, time-limited, and configurable with download/upload caps.
Built with FastAPI, server-rendered HTML, and SQLite or PostgreSQL.
Screenshots coming soon.
- Upload one or more files and share a public download link
- Optional password, expiry date, and download limit (
0= unlimited) - Email share links to recipients (when SMTP is configured)
- Optional notification when someone downloads
- Enable or disable a link without deleting it
- Edit transfers after creation, including extending expiry on expired links
- Regenerate public token (invalidates the old link)
- Create a link for external users to upload files to you
- Staged multi-file upload with optional password and upload limits
- Download received uploads as a ZIP from the dashboard
- Same enable/disable, edit, expiry, and link-regeneration workflow as transfers
- Unguessable link tokens (
secrets.token_urlsafe(32)) - Password unlock with signed cookies (no account required for recipients)
- ZIP or per-file download for transfers
- Rate limiting on public endpoints (30 requests/minute per IP by default)
- Unknown tokens redirect to login; expired or disabled links return 403/410 without treating them as attacks
- OAuth2 — Microsoft Entra ID out of the box (extensible to more providers)
- Local login — email/password backup on the login screen (can be disabled)
- First-boot setup wizard creates the initial admin account
- OAuth users are auto-provisioned on first login (matched by email)
| Area | What you can configure |
|---|---|
| Branding | App name, logo, color scheme |
| Limits | Max file size, default expiry, max share lifetime, default download limit, purge grace period, file extension blocklist, local login, user-sent share emails |
| SMTP | Outbound mail for share links and notifications |
| Email templates | Editable Jinja2 subjects and HTML bodies for all notification types |
| Shares | Overview of all transfers and requests across users; edit or delete any share |
| Users | Create users, promote/demote admins, reset passwords, delete accounts |
| Impressum | Optional legal notice page (Markdown) |
| Audit log | Recent admin and system actions |
When SMTP is configured, the app can send:
- Share link emails to transfer recipients
- File-request link emails
- Upload-received notifications to the request owner
- Download notifications (when enabled per transfer)
- Expired unused — when a transfer or file request expires with zero downloads/uploads
- Deletion reminder — configurable days before permanent purge (global setting;
0= disabled)
All templates (subject and body) are editable under Admin → Email templates.
A background job runs every 15 minutes to:
- Mark shares past their expiry date as expired
- Email owners when an expired share had no downloads or uploads
- Email owners before auto-delete, if Deletion reminder is enabled in admin limits
- Permanently delete expired shares after a configurable purge grace period (days;
0= auto-delete disabled)
During the grace period, shares show an Expired and Deletion pending badge in the UI. Extending expiry on the edit page clears the expired state and resets notification flags.
Transfers and file requests display clear status badges: Active, Expired, Disabled, download/upload limit reached, Password protected, Notifications (transfers with download alerts), and Deletion pending.
git clone https://github.com/your-org/owntransfer.git
cd owntransfer
cp .env.example .env
# Set SECRET_KEY in .env (see Configuration below)
docker compose up --buildOn first boot, the app prints a one-time setup token to the container logs. Look for a line like:
Setup token (required once): <long-random-string>
Open http://localhost:8080/setup, paste that token, and create your admin account. If you restart the container before finishing setup, check the logs again — a new token is generated each time.
To use a fixed token instead (for example in automation), set SETUP_TOKEN in .env before starting the app.
Most settings live in the database and are managed from the admin panel after first boot. Environment variables seed defaults on startup.
See .env.example for the full list. Important variables:
| Variable | Purpose |
|---|---|
SECRET_KEY |
Session signing — required; generate a long random string |
SETUP_TOKEN |
Optional fixed token for the first-boot setup wizard; when unset, a random token is logged at startup |
BASE_URL |
Public URL used in share links and emails (e.g. https://transfer.example.com) |
PUBLIC_SCHEME |
http or https — override scheme behind a TLS-terminating reverse proxy (OAuth callbacks) |
DB_BACKEND |
sqlite (default) or postgres |
SQLITE_PATH |
SQLite database file (default /data/owntransfer.db) |
DATABASE_URL |
Optional full database URL override |
UPLOAD_DIR |
Local file storage path (default /data/uploads) |
UPLOAD_CONCURRENCY |
Default for Admin → Limits → Parallel file uploads on first boot (default 5, range 1–50) |
DISPLAY_TIMEZONE |
IANA timezone for UI and emails (e.g. Europe/Berlin) |
ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET |
Microsoft OAuth (optional) |
SMTP_* |
Email defaults (overridable in admin after first boot) |
TRUST_PROXY_HEADERS |
Set true behind a reverse proxy so client IPs and URLs are correct |
TRUSTED_PROXY_HOPS / TRUSTED_PROXY_IPS |
Required when proxy headers are enabled — limit which peers may send X-Forwarded-For / X-Real-IP |
docker compose -f docker-compose.yml -f docker-compose.postgres.yml up --buildOr set DB_BACKEND=postgres and DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/db in .env.
Any environment variable supports a VAR_FILE companion that points to a file whose contents become VAR. This works for all settings (for example SECRET_KEY_FILE, POSTGRES_PASSWORD_FILE, ENTRA_CLIENT_SECRET_FILE). When both are set, the file value takes precedence.
Run OwnTransfer behind nginx, Caddy, or Traefik in production. Set BASE_URL to your public HTTPS URL. If the app receives HTTP internally behind TLS termination, set PUBLIC_SCHEME=https so OAuth callbacks and other generated URLs use HTTPS. Enable TRUST_PROXY_HEADERS=true so security logging and rate limiting see the real client IP (and so X-Forwarded-Proto is honored when present). Also set TRUSTED_PROXY_IPS to the proxy's address (for example 127.0.0.1 or your Docker network CIDR). If TRUSTED_PROXY_IPS is empty, the app logs a startup warning because any client could spoof forwarded headers.
Large multi-file uploads need a longer proxy timeout than the common 60-second default. Without this, uploads fail with 502 Bad Gateway once the limit is hit. Examples:
nginx
proxy_read_timeout 600s;
proxy_send_timeout 600s;
client_body_timeout 600s;Traefik (Docker labels on the router/service, or static/dynamic file):
# Dynamic configuration (e.g. dynamic.yml)
http:
services:
owntransfer:
loadBalancer:
servers:
- url: "http://owntransfer-app-1:8080"
responseForwarding:
flushInterval: 100ms
middlewares:
owntransfer-timeouts:
buffering:
maxRequestBodyBytes: 0 # disable buffering for large uploads
routers:
owntransfer:
rule: "Host(`your-domain`)"
service: owntransfer
middlewares:
- owntransfer-timeoutsFor Traefik v2/v3, set transport timeouts on the serversTransport (or per-service in newer versions):
# dynamic.yml — serversTransport
serversTransports:
owntransfer-transport:
forwardingTimeouts:
dialTimeout: 30s
responseHeaderTimeout: 600s
idleConnTimeout: 600sLink the transport to your service via serversTransport: owntransfer-transport@file.
Caddy — increase response_header_timeout / upstream timeouts in your site block as needed.
-
Azure Portal → App registrations → New registration
-
Redirect URI (Web):
https://your-domain/auth/oauth/entra/callback -
Create a client secret under Certificates & secrets
-
API permissions:
openid,email,profile(Microsoft Graph, delegated) -
Add to
.env:ENTRA_TENANT_ID=your-tenant-id ENTRA_CLIENT_ID=your-client-id ENTRA_CLIENT_SECRET=your-client-secret BASE_URL=https://your-domain
Promote OAuth users to admin in Admin → Users after their first login.
The included docker-compose.yml mounts a named volume at /data for the database and uploads. For production, place a reverse proxy in front and set a strong SECRET_KEY.
Mount a volume (or bind mount) at /data so the SQLite database and uploaded files survive container restarts. Set UPLOAD_DIR=/data/uploads and SQLITE_PATH=/data/owntransfer.db.
The app uses a storage abstraction (app/services/storage/). Local disk is the default; S3-compatible backends can be wired in without changing business logic.
Run the same container image with a persistent volume claim for /data, configure secrets for SECRET_KEY and database credentials, and expose the service through an ingress controller.
- First-boot setup requires a one-time token (printed to container logs, or set via
SETUP_TOKEN) - Public link tokens are cryptographically random; passwords are bcrypt-hashed
- Rate limiting on public download and upload routes
- Session cookies are HTTP-only
- File extension blocklist configurable in admin
- Audit log for administrative actions
OwnTransfer writes structured WARNING lines to stdout for events that indicate probing or brute force:
| Event | Trigger |
|---|---|
invalid_login |
Failed local login |
invalid_transfer_link |
Unknown /d/{token} |
invalid_request_link |
Unknown /r/{token} |
Expired, disabled, or limit-reached links are not logged — the token exists; access is denied with 403/410.
Example log line:
2026-06-22 12:00:00 WARNING [owntransfer.security] event=invalid_login ip=203.0.113.10 method=POST [email protected]
Filter — save as /etc/fail2ban/filter.d/owntransfer.conf:
[Definition]
failregex = ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} WARNING \[owntransfer\.security\] event=(invalid_login|invalid_transfer_link|invalid_request_link) ip=<HOST>.*$
ignoreregex =
datepattern = ^%%Y-%%m-%%d %%H:%%M:%%SJail — save as /etc/fail2ban/jail.d/owntransfer.conf (adjust logpath):
[owntransfer]
enabled = true
port = http,https
filter = owntransfer
logpath = /var/log/owntransfer/app.log
maxretry = 5
findtime = 600
bantime = 3600Point logpath at your application log file. With Docker, forward container stdout to a file or use your platform's log shipping. Enable TRUST_PROXY_HEADERS behind a reverse proxy so ip= is the real client address.
The UI supports English (en), German (de), and Spanish (es). More languages can be added by extending SUPPORTED_LOCALES in app/i18n/__init__.py and creating a new catalog under app/locales/.
How the active language is chosen:
localecookie — set when a user picks a language in the footer switcherAccept-Languagebrowser header — used on first visitDEFAULT_LOCALEenvironment variable — fallback (defaulten)
Admin-editable legal pages and custom email template overrides are not localized; built-in UI strings and default email templates are.
Developer workflow (after changing user-facing strings):
pybabel extract -F babel.cfg -o app/locales/messages.pot .
pybabel update -i app/locales/messages.pot -d app/locales -l de
pybabel update -i app/locales/messages.pot -d app/locales -l es
# Edit app/locales/de/LC_MESSAGES/messages.po (or run scripts/fill_de_translations.py)
# Edit app/locales/es/LC_MESSAGES/messages.po (or run scripts/fill_es_translations.py)
pybabel compile -d app/localesRequirements: Python 3.11+, Node.js 18+ (for Tailwind CSS builds)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
export DB_BACKEND=sqlite
export SQLITE_PATH=./owntransfer.db
export UPLOAD_DIR=./uploads
export SECRET_KEY=dev-secret
uvicorn app.main:app --reload --port 8080
pytestAfter changing HTML templates or frontend classes in app/static/*.js, rebuild the CSS locally:
npm install
npm run build:cssDocker images (including GitHub Actions releases) build app/static/tailwind.css automatically during docker build.
The database schema is created automatically on startup via ensure_schema in the application lifespan.
Copyright (C) 2026 Jan K.
OwnTransfer is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License v3.0 (AGPL-3.0).
If you run a modified version of this software as a network service, you must make the corresponding source code available to users interacting with it over a network.