A self-hosted file sharing platform with a clean web interface, ShareX integration, and API access. Upload screenshots, files, and media — then instantly share them via short links. Full documentation is available at sharely.christian.pizza/docs.
- Web UI — Drag-and-drop uploads, searchable gallery with type filters (images, video, audio, PDF, code), fully mobile-responsive
- Chunked uploads — Files up to 2 GB supported via parallel multi-part chunked upload (automatic, no client config needed)
- ShareX integration — One-click
.sxcuconfig download for automatic screenshot uploads from Windows - API uploads — Bearer token authentication, compatible with curl, wget, and any HTTP client
- File viewing — Images zoom inline, videos/audio stream with seek support (HTTP Range), PDFs render in-browser, code files syntax-highlighted
- File download — Force-download endpoint (
/f/<id>/download) separate from the inline viewer - Embed modes — Per-user toggle between embed (rich HTML page with Open Graph / Twitter Card metadata) and raw (direct file redirect for native social embeds); social media bots (Discord, Telegram, Twitter, etc.) are detected automatically
- Share links — Generate per-file share links (
/s/<token>) with optional password protection, expiry date, and download limit - QR codes — Every share link and collection link can be shown as a QR code and downloaded as a PNG for quick mobile sharing
- Collections — Group files into named collections with optional password and expiry date, shareable via a single link; download the whole collection (or any multi-selection in the gallery) as a ZIP
- Self-destructing uploads — Give any file an expiry date and/or a maximum download count; once reached it becomes unavailable everywhere and is reclaimed by the cleanup job
- Tags & descriptions — Tag files for organisation (with a personal set of predefined tags) and add a free-text description; the gallery search matches file name, tags and description
- Metadata stripping — Optionally remove EXIF/GPS and other metadata from JPEG/PNG uploads site-wide (privacy)
- Thumbnails — Video and PDF files get auto-generated JPEG thumbnails (requires ffmpeg / ghostscript, bundled in the Docker image)
- Avatars — Users can upload a profile avatar (JPEG, PNG, GIF, WebP, max 2 MB)
- Internationalization — UI and email templates available in 8 languages: English, Deutsch, Français, Español, Italiano, Português, 日本語, 中文
- Email verification & password reset — Requires SMTP configuration; tokens expire after 24 h (verification) / 1 h (reset)
- User management — Role-based access control (admin/user), account activation/deactivation, custom folder names
- Storage quotas — Per-user storage limits with a configurable site-wide default; uploads are rejected once a user reaches their quota, and each user sees their own usage in Settings
- Admin dashboard — Stats overview, manage all users and files, audit log with CSV export
- Real-time UI — WebSocket-powered live updates: file view counters, gallery refresh on upload/delete, admin stats and audit log stream in real time
- XBackBone migration — Import your existing XBackBone installation including files and metadata
- Short links — Every file gets a random 24-character short ID (e.g.
/f/3f9a…); older 8-character IDs keep working - Delete tokens — Each file carries a unique deletion token so ShareX (or any client) can delete files without a session
- Security — Dangerous file types (
.exe,.bat,.sh,.php,.ps1, …) are blocked on upload; only passive media is shown inline, everything else is sent as a download; optional encryption at rest; token-protected first-run setup; sessions revalidated on every request; rate limiting on uploads, logins, password checks, account actions, ID lookups and WebSocket messages; security headers (CSP, HSTS, Referrer-Policy); access logs without full IPs or tokens - Docker-ready — Single
docker compose upgets you running - GDPR-ready — Privacy policy & Terms of Service pages (configurable), data export (Art. 20), account deletion (Art. 17), audit log with 90-day auto-expiry, configurable file retention, API key hashing, and optional Cloudflare Analytics cookie consent out of the box
| Gallery | File View |
|---|---|
![]() |
![]() |
| Upload | Admin Dashboard |
|---|---|
![]() |
![]() |
Requirements: Docker and Docker Compose
git clone https://github.com/Christianoooooo/sharely.git
cd sharely
cp .env.example .envEdit .env with your settings (see Configuration), then:
docker compose up -dThe app is now running at http://localhost:3000. The setup page asks for a setup token, which the app prints to its log on first start (docker compose logs app); the account you create there is the admin.
Requirements: Node.js 22+ (24 recommended), MongoDB
# Backend
npm install
npm run dev
# Frontend (separate terminal)
cd client
npm install
npm run dev- Backend:
http://localhost:3000 - Frontend dev server:
http://localhost:5173
Copy .env.example to .env and adjust the values:
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Port the server listens on |
BIND_ADDRESS |
127.0.0.1 |
Host address the Docker port is published on. Keep loopback behind a reverse proxy; 0.0.0.0 only behind an external firewall or without a proxy (see "Cloudflare Tunnel / proxy in a container") |
TRUST_PROXY |
1 |
Number of trusted reverse-proxy hops (e.g. 2 for Cloudflare → nginx); must match your setup or client IPs/rate limits are wrong |
ENCRYPTION_KEY |
— | 32-byte key (openssl rand -base64 32). If set, new uploads and thumbnails are stored encrypted (see "Encryption at rest"). Losing it makes encrypted files unreadable |
INSTALL_TOKEN |
(random, printed to the log) | Token required to complete the first-run setup |
MONGO_ROOT_PASSWORD |
— | MongoDB root password (required) |
MONGO_APP_USER |
appuser |
MongoDB application user |
MONGO_APP_PASSWORD |
— | MongoDB application user password (required) |
MONGO_DB_NAME |
sharely |
MongoDB database name |
SESSION_SECRET |
— | Secret for session encryption — use a long random string (required) |
BASE_URL |
http://localhost:3000 |
Public base URL for generated share links, no trailing slash |
SITE_NAME |
sharely |
Site name shown in Open Graph embeds |
MAX_FILE_SIZE_MB |
100 |
Maximum upload file size in MB (chunked uploads can go up to 2 GB regardless) |
ALLOW_REGISTRATION |
true |
Set to false to disable public sign-up (admin-only user creation) |
SMTP_HOST |
— | SMTP server hostname; leave blank to disable email features |
SMTP_PORT |
587 |
SMTP port |
SMTP_SECURE |
false |
true for implicit TLS (port 465), false for STARTTLS (port 587) |
SMTP_USER |
— | SMTP authentication username |
SMTP_PASS |
— | SMTP authentication password |
SMTP_FROM |
(SMTP_USER) | From address shown in outgoing emails |
LDAP_URL |
— | LDAP server URL (ldaps:// recommended; ldap:// sends passwords unencrypted); leave blank to disable LDAP login |
LDAP_BIND_DN |
— | DN of the service account used to search for users; blank for an anonymous bind |
LDAP_BIND_PASSWORD |
— | Password for LDAP_BIND_DN |
LDAP_SEARCH_BASE |
— | Base DN under which users are searched, e.g. ou=people,dc=example,dc=com |
LDAP_SEARCH_FILTER |
(uid={{username}}) |
Search filter; {{username}} is replaced with the (escaped) login name |
LDAP_ATTR_EMAIL |
mail |
Directory attribute read as the user's email address |
LDAP_TLS_REJECT_UNAUTHORIZED |
true |
Set to false to accept self-signed/untrusted ldaps:// certificates |
UPDATE_ENABLED |
false |
Enable one-click self-update from the admin panel (see Automatic Updates) |
UPDATE_CHANNEL |
release |
How updates are detected: release (latest GitHub release tag) or branch (new commits on UPDATE_BRANCH) |
UPDATE_BRANCH |
main |
Branch the updater pulls, and compares against in branch channel |
UPDATE_REPO |
Christianoooooo/sharely |
GitHub repo checked for new releases |
COMPOSE_PROJECT_NAME |
sharely |
Compose project name; must match between host and updater |
Generate a secure session secret:
openssl rand -hex 32When SMTP_HOST is set, sharely enables:
- Email verification — new accounts must verify their address before logging in; tokens expire after 24 hours
- Email change confirmation — a verification link is sent to the new address
- Password reset — "Forgot password" flow with a 1-hour reset token (rate-limited to 5 requests per hour)
Email subjects and body copy are translated into all 8 supported languages and sent in the user's chosen language.
If SMTP_HOST is left blank, all email-dependent features are hidden from the UI and registration proceeds without verification.
When LDAP_URL is set, users can log in with their directory credentials. Authentication uses search + bind: the app binds with LDAP_BIND_DN (or anonymously), searches LDAP_SEARCH_BASE using LDAP_SEARCH_FILTER, and then re-binds as the found entry with the supplied password.
- Local-first — an existing local account always authenticates against its local password; the directory is only consulted for LDAP accounts and usernames with no local account.
- Auto-provisioning — on the first successful LDAP login a local user record is created (role
user) so folders, quotas and sessions work as usual. The account is tied to the directory entry's DN, not to the typed name. The login name must follow the usual username rules (3–32 characters, letters, digits,-,_) and must not differ only in case from an existing account. Nothing is provisioned before the installation is complete. The email fromLDAP_ATTR_EMAILis adopted when available, and no MX check is performed on it. - LDAP accounts have no usable local password: "change password" and "forgot password" do not apply to them, and other actions that ask for the password (change username or email, delete account) take the directory password. Promote an LDAP user to admin from the admin panel like any other account.
- Usernames that exist in the directory cannot be taken by self-registration or renaming.
- Removing or disabling a user in the directory blocks new logins only. Existing sessions and the API key keep working until you also deactivate the account in the admin panel.
If LDAP_URL is left blank, login behaves exactly as before.
The admin panel (Admin → Site Settings → Version & Updates) always shows the installed version and whether an update is available.
How "update available" is determined depends on UPDATE_CHANNEL:
release(default) — the installed version is compared against the latest GitHub release tag. Use this if you cut a release for each version.branch— the installed commit is compared against the tip ofUPDATE_BRANCH. An update is available whenever there are new commits on the branch, so you don't need to create a release. The installed version/commit is reported by the updater container, so this channel needs the updater running.
Optionally, admins can apply updates with one click. This is handled by a dedicated updater container — the only component with Docker socket access. The web app never touches the socket: it just drops a request file into a shared volume, and the updater runs git pull + docker compose build app thumbnailer + docker compose up -d app thumbnailer.
⚠️ Security note: the updater mounts the Docker socket, which is equivalent to root on the host. Enable self-update only if you accept that trade-off. If the web app is compromised, the worst it can trigger is a rebuild from your configured branch — it cannot run arbitrary host commands.
To enable it:
- Deploy from a git checkout (the default Quick Start does this) — the updater pulls into that checkout.
- Set
UPDATE_ENABLED=truein.env. KeepCOMPOSE_PROJECT_NAMEidentical to the name used when you first ran compose (defaultsharely). - Start the stack including the updater (it lives behind the
updaterprofile, so a plaindocker compose upnever starts the privileged container):
docker compose --profile updater up -d --buildWhen you click Install update, the app container is rebuilt and restarted; it is briefly unavailable. The page reports progress and reconnects automatically. Leave UPDATE_ENABLED=false (and omit the profile) to keep the panel notification-only.
Updating the updater itself: the updater only rebuilds the
appandthumbnailercontainers, never itself. After changing anything underupdater/, rebuild it once manually:docker compose --profile updater up -d --build updater
Get your API key from Settings → API Key in the web UI.
Upload a file:
curl -X POST https://your-domain.com/api/upload \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "sharex=@/path/to/file.png"The response contains the URL to the uploaded file and a deletionUrl that can be used to delete the file without a session (used automatically by ShareX).
Delete a file:
curl -X DELETE https://your-domain.com/api/delete/SHORT_ID \
-H "Authorization: Bearer YOUR_API_KEY"List your files:
curl https://your-domain.com/api/gallery \
-H "Authorization: Bearer YOUR_API_KEY"For large files the web UI automatically switches to chunked upload mode (files above ~250 MB). Chunks of 10–20 MB are uploaded in parallel (3–5 concurrent chunks). The API also exposes the chunked upload endpoints for programmatic use — see /api/upload/chunk/init, /api/upload/chunk/:sessionId, and /api/upload/chunk/:sessionId/finalize.
- Log in and go to Upload
- Click Download ShareX Config
- Open the downloaded
.sxcufile — ShareX will import it automatically - Screenshots and uploads are now sent directly to your instance
ShareX receives a deletionUrl in the upload response and can delete files without re-authenticating.
Any file can have one or more share links generated from the file view. Each link is served at /s/<token> and supports:
- Password protection — visitors must enter a password before accessing the file
- Expiry date — the link becomes invalid after the configured date
- Download limit — the link is disabled after N downloads (-1 = unlimited)
Share links are independent of the owner's account. Deleting a share link does not delete the underlying file.
Collections group multiple files under a single shareable link. Create and manage them from the Collections page. Each collection supports:
- Custom name and description
- Optional password and expiry date
- Adding / removing individual files
Collections are served at /c/<shortId> and respect the same password/expiry rules as share links.
Files can be tagged for organisation and filtering. Tags are stored per file and are searchable in the gallery.
To speed up tagging, define a personal list of predefined tags under Settings → Preferences → Tag Management. These appear as one-click suggestions when tagging files.
The app maintains a persistent WebSocket connection (/ws) for all authenticated clients. The protocol uses a request/response pattern plus server-initiated broadcasts.
The handshake is rejected unless Origin matches Host. Session, role and account state are rechecked on every message and every 30 seconds, so logout, deactivation, password changes and demotion take effect on open connections. Messages are limited to 64 KiB and rate-limited per connection; each IP may hold up to 20 connections.
Request format (client → server):
{ "id": "unique-request-id", "action": "action:name", "payload": {} }Response format (server → client):
{ "id": "unique-request-id", "data": {}, "error": null }Broadcast format (server → relevant clients):
{ "event": "event:name", "data": {} }| Action | Auth | Description |
|---|---|---|
site-settings:get |
— | Public site settings |
auth:me |
User | Current session user |
file:get |
User | File details (increments view count) |
file:list |
User | Own files (admins see all) |
file:delete |
User | Delete a file |
user:get-key |
User | API key prefix |
user:regen-key |
User | Regenerate API key |
user:change-password |
User | Change password |
user:change-username |
User | Change username |
user:change-email |
User | Change email (sends verification) |
user:change-language |
User | Set UI language |
user:change-embed-mode |
User | Toggle embed / raw mode |
user:resend-verification |
User | Re-send verification email |
user:export |
User | Export account data as JSON |
user:delete-account |
User | Delete account and all files |
admin:stats |
Admin | Dashboard statistics |
admin:settings:get |
Admin | Read site settings |
admin:settings:update |
Admin | Update site settings |
admin:users:list |
Admin | List all users |
admin:users:create |
Admin | Create user |
admin:users:toggle |
Admin | Activate / deactivate user |
admin:users:role |
Admin | Change user role |
admin:users:delete |
Admin | Delete user |
admin:users:regen-key |
Admin | Regenerate a user's API key |
admin:users:password |
Admin | Set a user's password |
admin:users:folder |
Admin | Change a user's folder name |
admin:files:list |
Admin | List all files |
admin:audit-log:list |
Admin | Paginated audit log |
| Event | Recipients | Payload |
|---|---|---|
file:uploaded |
Uploader only | { shortId, uploaderId } |
file:deleted |
Owner only | { shortId, uploaderId } |
file:view |
Owner + admins | { shortId, views } |
user:created |
Admins | { id, username, role } |
user:deleted |
Admins | { id } |
user:updated |
Admins | { id, …changed fields } |
audit:log |
Admins | Full AuditLog object |
settings:updated |
Admins | Updated SiteSettings object |
stats:invalidate |
Admins | {} (trigger a stats re-fetch) |
WebSocket support requires the Upgrade and Connection headers to be forwarded:
server {
listen 443 ssl;
server_name files.example.com;
client_max_body_size 2100M;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Make sure BASE_URL in .env matches your public domain.
Note on
client_max_body_size: Set this at least as large as your largest expected chunked segment plus some headroom. For 2 GB file support,2100Mis a safe value.
By default Docker publishes the app port only on 127.0.0.1 (BIND_ADDRESS). A proxy running directly on the host, like the nginx example above, reaches it. A proxy or cloudflared running in its own container does not, and answers with 502 Bad Gateway.
- Recommended: attach the proxy container to the sharely Docker network (
<project>_default, by defaultsharely_default) and point it tohttp://app:3000. No port needs to be published then:To keep this across restarts, declare the network asdocker network connect sharely_default <cloudflared-or-proxy-container>
externalin the proxy's compose file. - With an external firewall that blocks port 3000 from the internet (e.g. Hetzner Cloud Firewall): set
BIND_ADDRESS=0.0.0.0in.env. Do not do this without such a firewall: Docker bypasses host firewalls likeufw, and anyone reaching the port directly bypasses TLS and can spoofX-Forwarded-For, which defeats the rate limits.
The proxy must pass on the original Host header (WebSocket connections are rejected if Origin and Host differ) and X-Forwarded-Proto (needed for secure cookies and HSTS). Cloudflare Tunnel and the nginx example do this by default; do not set httpHostHeader in the tunnel configuration. After setting up, check that the audit log shows real client IPs; otherwise adjust TRUST_PROXY.
Upgrade note: Earlier versions published port 3000 on all interfaces. If your proxy runs in a container, set
BIND_ADDRESSor attach the network as described above before updating.
With ENCRYPTION_KEY set, sharely encrypts every new upload and its thumbnail with AES-256-GCM before storing it (one key per file, derived from the master key). Files are decrypted on the fly when served, including Range requests and ZIP downloads.
- Generate the key once with
openssl rand -base64 32, put it in.envand back it up separately: without it, encrypted files cannot be recovered. Changing or removing the key later is not supported. - Existing files stay unencrypted until you run the migration (back up the
uploadsvolume first):docker compose exec app npm run migrate:encrypt - Not covered: pastes, file names and other metadata in MongoDB, avatars, and the process memory of the running server. While an upload is processed (chunk assembly, metadata stripping, thumbnail generation) it briefly exists unencrypted on disk.
- The admin setting for disk/volume encryption only declares volume encryption in the privacy policy and is independent of this.
Video and PDF files get a JPEG thumbnail automatically on upload. Thumbnails are stored in uploads/.thumbnails/ and served at /f/<shortId>/thumb.
The Docker image includes ffmpeg (video) and ghostscript (PDF), so no extra setup is needed there. With Docker Compose they run in a separate thumbnailer container without network, secrets or access to stored uploads, so a malicious video or PDF that exploits one of these tools cannot reach the app's data. If that container is not running, thumbnails are skipped.
Upgrading from an older version: run
docker compose up -d --buildonce so thethumbnailercontainer is created. If you use the updater, also rebuild it once (docker compose --profile updater up -d --build updater); older updaters only recreate theappcontainer.
For local development, install the tools manually:
# Debian / Ubuntu
sudo apt install ffmpeg ghostscript
# macOS
brew install ffmpeg ghostscriptIf the tools are unavailable, thumbnail generation is silently skipped and the gallery shows a styled colour-coded placeholder instead.
To generate thumbnails for files that were uploaded before this feature was introduced, run the migration script once inside the container:
docker exec -it <container-name> npm run migrate:thumbnailsOr locally:
npm run migrate:thumbnailsThe script is idempotent — already-generated thumbnails are skipped.
If you're migrating from XBackBone:
- Log in as admin and go to Admin → Import
- Provide the path to your XBackBone
database.dbfile - Provide the path to your XBackBone
storage/directory - Run a preview first, then confirm the import
Users are matched by username. Files without a matching user are assigned to a fallback user you specify. The import handles multiple XBackBone schema versions and searches storage subdirectories automatically. It is idempotent — re-running it skips already-imported files.
The project ships with a Playwright test suite covering the main user flows (upload, gallery, share links, tags, admin actions).
Run tests (requires the app to be running):
# Headless
npm run test:e2e
# Interactive UI mode
npm run test:e2e:uiThe global setup in e2e/global-setup.js creates the necessary test users automatically. Make sure BASE_URL in .env points to a running instance before executing the suite.
sharely/
├── app.js # Express entry point
├── src/
│ ├── config/db.js # MongoDB connection
│ ├── models/ # Mongoose schemas (User, File, Paste, Collection, ShareLink, AuditLog, SiteSettings)
│ ├── middleware/ # Auth, upload handling, file block list, lookup rate limit
│ ├── jobs/
│ │ └── retentionCleanup.js # Scheduled file cleanup, stale chunk/plaintext leftovers
│ ├── utils/
│ │ ├── fileCrypto.js # Encryption at rest (AES-256-GCM, Range-capable)
│ │ ├── storeUpload.js # Encrypt uploads, thumbnail from plaintext copy
│ │ ├── mime.js # MIME normalization and inline whitelist
│ │ ├── deleteUserData.js # Complete account erasure
│ │ ├── setupToken.js # First-run setup token
│ │ ├── generateThumbnail.js # ffmpeg/ghostscript thumbnail generation
│ │ └── mailer.js # Nodemailer wrapper with i18n templates
│ ├── ws.js # WebSocket server (all WS actions & broadcasts)
│ └── routes/ # REST routes (auth, api, files, shares, collections, import, install)
├── client/ # React frontend (Vite + Tailwind)
│ └── src/
│ ├── i18n/ # i18next config + 8 locale JSON files
│ ├── hooks/
│ │ └── useWebSocket.js # React hook for WS connection & events
│ ├── pages/ # Upload, Gallery, FileView, Collections, Settings, Admin pages
│ └── components/ # UI components
├── e2e/ # Playwright end-to-end tests
├── scripts/
│ ├── setup-db.js
│ ├── migrate-uploads-to-user-folders.js
│ ├── generate-missing-thumbnails.js
│ └── encrypt-existing-files.js # encrypt files stored before ENCRYPTION_KEY was set
├── uploads/
│ ├── .thumbnails/ # auto-generated JPEG thumbnails
│ ├── .chunks/ # temporary chunked-upload segments
│ └── .avatars/ # user profile avatars
├── docker-compose.yml
└── .env.example
| Layer | Technology |
|---|---|
| Backend | Node.js, Express |
| Database | MongoDB (Mongoose) |
| Real-time | WebSocket (ws) |
| Nodemailer | |
| Frontend | React 18, React Router, Vite |
| Styling | Tailwind CSS, Radix UI |
| i18n | i18next |
| File uploads | Multer (standard) + chunked upload (custom) |
| Auth | express-session, bcryptjs |
| Container | Docker, Docker Compose |
Sharely is designed with privacy in mind. The following features support GDPR compliance for operators in the EU:
| Feature | GDPR Article |
|---|---|
| Privacy Policy page (configurable via Admin Panel) | Art. 13/14 – Transparency |
| Terms of Service page (configurable via Admin Panel) | Art. 13/14 – Transparency |
| Data export (JSON download of account + file metadata with full URLs) | Art. 20 – Portability |
| Account deletion by the user or an admin (removes files, pastes, collections, share links and avatar; anonymizes the audit log) | Art. 17 – Right to erasure |
| Audit log (all access and admin actions, 90-day auto-expiry via MongoDB TTL) | Art. 5(2) – Accountability |
| Audit log CSV export for regulators | Art. 5(2) – Accountability |
| Configurable file retention (auto-delete after N days) | Art. 5(1)(e) – Storage limitation |
| API keys stored as SHA-256 hashes, never in plaintext | Art. 32 – Security |
| Passwords stored as bcrypt hashes (12 rounds) | Art. 32 – Security |
Optional encryption of stored files and thumbnails (ENCRYPTION_KEY) |
Art. 32 – Security |
| Encryption-at-rest documentation (operator self-declaration of volume encryption) | Art. 32 – Security |
| Access log with truncated IPs and redacted tokens | Art. 5(1)(c) – Data minimisation |
| Cookie consent notice for Cloudflare Analytics (optional) | Art. 13 – Transparency |
Note for operators: GDPR compliance is ultimately the operator's responsibility. Before going live, fill in your operator details under Admin → Site Settings (name, address, contact email). Consider whether you need a Data Processing Agreement (DPA) with your hosting provider.
MIT




