# SealShare
A simple, self-hosted file sharing solution built with Laravel. Upload files, get a shareable link, done. All files are encrypted at rest with AES-256-GCM.
**Website:** [sealshare.nonameweb.ch](https://sealshare.nonameweb.ch)
## Screenshots
## Features
- **File Uploading** — Drag & drop or browse to upload single/multiple files and folders with real-time progress; large files go up in chunks, each retried on its own if the connection drops
- **Shareable Links** — Each upload generates a unique link for recipients, also as a QR code (saved as a PNG) or through the device's share sheet
- **Encryption at Rest** — Files are encrypted in the uploader's browser, chunk by chunk with AES-256-GCM, before they are sent, and are stored only in encrypted form; with a share password the share's key is wrapped with a key derived from it (Argon2id) and never stored as it is. It is not end-to-end encryption: the server issues the key, checks each chunk, and decrypts the files for downloads
- **Password Protection** — Optionally protect shares with a password, typed or generated (random characters or a passphrase, as the admin configures) and copied on the upload page or next to the new link
- **Expiration** — Shares auto-expire after a configurable duration (1 hour to 30 days)
- **Download Limits** — Set a maximum number of downloads per share
- **ZIP Downloads** — Download all files in a share as a single ZIP archive, streamed as it is built, whatever the files' size
- **Auto-Cleanup** — Expired shares and files are automatically deleted (hourly)
- **Admin Dashboard** — View, manage, and delete all shares
- **Admin Settings** — Configure upload limits, storage quotas, branding, and more
- **Site Branding** — Custom logo, title, and description
- **Colour Profiles** — Eight Material 3 colour profiles (Indigo, Blue, Teal, Green, Amber, Rose, Violet, Graphite); the admin picks one for every page, mail and error page
- **System Password** — Optional global password gate to restrict upload access
- **User Authentication** — Login, password reset, email verification
- **Two-Factor Authentication** — TOTP-based 2FA via Laravel Fortify
- **Light and Dark Themes** — Material 3 Expressive design that follows the system theme, or light or dark by choice
- **Setup Wizard** — First-run wizard to create the initial admin account
## Tech Stack
| Layer | Technology |
|-------|-----------|
| **Framework** | Laravel 13 |
| **Application Server** | FrankenPHP (via Laravel Octane) |
| **Frontend** | Livewire 4, Tailwind CSS 4, [Livewire Material](https://gitea.nonameweb.ch/noNameWEB/livewire-material) (Material 3 Expressive) |
| **Authentication** | Laravel Fortify |
| **Encryption** | Chunked AES-256-GCM (WebCrypto in the browser), keys wrapped with Argon2id |
| **ZIP Downloads** | [ZipStream-PHP](https://packagist.org/packages/maennchen/zipstream-php) |
| **Testing** | Pest 5 with browser tests (Playwright) |
| **Code Style** | Laravel Pint |
| **Build Tool** | Vite |
## Installation — Development
### Docker (recommended)
```bash
# Build and start the dev container
docker compose -f docker-compose.dev.yml up -d --build
# View logs (including Vite output)
docker compose -f docker-compose.dev.yml logs -f
```
The app is available at `http://localhost:8000` with Vite HMR on port `5173`.
## Installation — Production
### Docker (recommended)
```bash
mkdir sealshare && cd sealshare
curl -O https://gitea.nonameweb.ch/noNameWEB/SealShare/raw/branch/main/docker-compose.example.yml
cp docker-compose.example.yml docker-compose.yml
# Generate an app key and paste it into docker-compose.yml
docker run --rm gitea.nonameweb.ch/nonameweb/sealshare:latest php artisan key:generate --show
# Edit docker-compose.yml — set APP_KEY and APP_URL, and choose how HTTPS is served (below)
# Then start:
docker compose up -d
```
Migrations run automatically on startup. Open your configured domain — the Setup Wizard will create the first admin account.
**Key environment variables:**
| Variable | Required | Description |
|----------|----------|-------------|
| `APP_KEY` | Yes | Laravel encryption key |
| `APP_URL` | Yes | Full URL (e.g. `https://share.example.com`) |
| `AUTO_HTTPS` | No | `true` to fetch a Let's Encrypt certificate for `SERVER_NAME` and serve HTTPS on port 443 (port 80 redirects); default `false`, plain HTTP on port 80 for a reverse proxy |
| `SERVER_NAME` | With `AUTO_HTTPS` | The domain to fetch the certificate for (e.g. `share.example.com`) |
| `UPLOAD_CHUNK_SIZE_MB` | No | Size of each encrypted chunk the browser sends; default `16` |
**HTTPS is required for uploads.** Files are encrypted in the uploader's browser with WebCrypto, which browsers only offer over HTTPS or on `localhost`; over plain HTTP the upload page says so and takes no files (downloads keep working). Either set `AUTO_HTTPS: "true"` with `SERVER_NAME` — ports 80 and 443 must be reachable from the internet — or put a reverse proxy that terminates TLS in front of port 80.
**Volumes:**
| Volume | Path | Purpose |
|--------|------|---------|
| `sealshare_storage` | `/app/storage/app` | Encrypted uploaded files |
| `sealshare_database` | `/app/database` | SQLite database |
| `caddy_data` | `/data` | TLS certificates |
| `caddy_config` | `/config` | Caddy configuration |
**Large files:**
Files go up in chunks of `UPLOAD_CHUNK_SIZE_MB`, one request each, so PHP's upload limits and a proxy's request timeout do not limit a file's size. What does:
| Limit | Where | Default |
|-------|-------|---------|
| Max file size / Max size per share | Admin → Settings | 100 MB / 2 GB |
| Storage quota | Admin → Settings | 20 GB — files still uploading count towards it |
| `UPLOAD_CHUNK_SIZE_MB` | Environment | `16` |
Behind a reverse proxy, its request body limit must be a little larger than a chunk (nginx: `client_max_body_size 32m;`), and `proxy_request_buffering off;` keeps nginx from writing each chunk to its own temporary files. `PHP_UPLOAD_MAX_FILESIZE` / `PHP_POST_MAX_SIZE` (default `64M`) only apply to the admin's logo upload. An upload no chunk reached for 4 hours is deleted by the hourly cleanup.
### Manual (without Docker)
```bash
git clone https://gitea.nonameweb.ch/noNameWEB/SealShare.git
cd SealShare
composer install --no-dev --optimize-autoloader
npm install && npm run build
cp .env.example .env
php artisan key:generate
# Edit .env — set APP_ENV=production, APP_DEBUG=false, APP_URL=https://your-domain.com
touch database/database.sqlite
php artisan migrate --force
php artisan storage:link
php artisan config:cache
php artisan route:cache
php artisan view:cache
```
Start with Octane:
```bash
php artisan octane:frankenphp --host=0.0.0.0 --port=80
```
Or point your web server (Nginx/Apache) to the `public/` directory for a traditional PHP-FPM setup.
Add the scheduler to your crontab:
```bash
* * * * * cd /path-to-sealshare && php artisan schedule:run >> /dev/null 2>&1
```
## License
This project is open-source software licensed under the [MIT License](LICENSE).
Generated passphrases draw from the [EFF Large Wordlist](https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases) by the Electronic Frontier Foundation, licensed under [CC BY 3.0 US](https://creativecommons.org/licenses/by/3.0/us/) (`resources/wordlists/eff-large-wordlist.txt`, without its four hyphenated words).