3 Commits
Author SHA1 Message Date
Andreas Reinhold / reiniandClaude Opus 5 606cc766f2 Add the SealShare website for sealshare.nonameweb.ch
docker / test (8.5) (push) Successful in 2m5s
docker / build-and-push (push) Failing after 9m38s
docker / release (push) Has been skipped
linter / quality (push) Successful in 1m8s
tests / ci (8.5) (push) Successful in 2m4s
website/ is hand-written HTML and CSS, uploaded as it is, like the
MailifySMS site: SealShare as software a company installs for its own
upload platform, how it works, features, a desktop/phone and light/dark
gallery of the generated screenshots, a dated and sourced comparison with
hosted transfer services and self-hosted tools, the Docker quick start,
FAQ and a privacy page for Plausible. Colours come from the app's scheme,
Google Sans Flex is served locally, nothing else loads from other hosts.

The README shows three screenshots and no longer calls the encryption
end-to-end. WebsiteTest guards missing files, other hosts, the screenshot
set and the encryption wording.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 12:38:49 +02:00
Andreas Reinhold / reiniandClaude Opus 5 e49bdf3eb3 Take the website's screenshots with one command
composer screenshots runs Pest browser tests in tests/Screenshots, outside
every test suite: fixed demo data under a frozen clock, desktop (MacBook
14, 2x) and phone (iPhone 15 Pro, 3x) in light and dark, each capture
published at once as WebP at two widths into website/img/screenshots.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 12:30:53 +02:00
Andreas Reinhold / reiniandClaude Opus 5 4470c2ed87 Give the dev container its own node_modules
npm installs the build tools' native binaries for the platform it runs
on, so a folder shared with the host only ever held Linux's or macOS's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 12:30:53 +02:00
65 changed files with 3178 additions and 2 deletions
+2
View File
@@ -7,3 +7,5 @@ Before planning or editing, find the row whose globs match the file's path and r
| config/livewire-material.php | .ai/rules/config.md |
| resources/css/material-scheme.* | .ai/rules/css.md |
| resources/views/livewire/share-download.blade.php | .ai/rules/livewire.md |
| tests/Screenshots/** | .ai/rules/screenshots.md |
| website/** | .ai/rules/website.md |
+9
View File
@@ -0,0 +1,9 @@
---
paths:
- 'tests/Screenshots/**'
---
# Screenshots
## Screenshots come from composer screenshots, before a release
Run `composer screenshots` whenever the interface changes and before a release; it builds assets and runs tests/Screenshots (not part of any test suite or CI), publishing WebP files to website/img/screenshots. Demo data (DemoData) and the clock are fixed so runs are reproducible. Traps: Pest only starts its browser for a test whose body calls `visit(` after whitespace; Livewire's temporary-upload cleanup must stay off under the frozen clock or it deletes the selected files; the in-process server's random port is shown as https://files.example.com and the QR redrawn for it; upload_max_filesize/post_max_size are set to 4G by the script so the admin settings hint does not show the machine's PHP limit.
+9
View File
@@ -0,0 +1,9 @@
---
paths:
- 'website/**'
---
# Website
## website/ is the live site, uploaded by hand
website/ is a faithful copy of sealshare.nonameweb.ch (METANET hosting), hand-written HTML/CSS with no build step, uploaded wholesale when it changes. Colours in css/theme.css are copied from resources/css/material-scheme.json — copy them again whenever the scheme is regenerated. The comparison tables are dated and every competitor value has a source from the product's own site, docs or repo; an unsourced value is "—", never a guess. Never call SealShare's encryption end-to-end (it encrypts at rest on the server). Nothing may load from another host except plausible.io. tests/Feature/WebsiteTest.php guards all of this.
+3
View File
@@ -25,6 +25,9 @@ Dockerfile
# Testing
tests
# Website (published separately)
website
phpunit.xml
.phpunit.cache
+2
View File
@@ -25,3 +25,5 @@ yarn-error.log
**/caddy
frankenphp
frankenphp-worker.php
/tests/Browser/Screenshots
+1
View File
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- The README called SealShare's encryption end-to-end. Files are encrypted at rest on the server; the README now says so, and that a share password's key is never stored.
- The admin dashboard passed its sort column and direction straight to the query, so a crafted Livewire request could order shares by any column or cause a server error. It now sorts only by the columns it shows and otherwise falls back to newest first.
## [1.2.0] - 2026-09-10
+13 -1
View File
@@ -2,11 +2,23 @@
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
<p>
<img src="website/img/screenshots/desktop/light/01-upload-800.webp" alt="Uploading files and folders with share options" width="49%">
<img src="website/img/screenshots/desktop/light/02-share-created-800.webp" alt="A new share with its link and QR code button" width="49%">
</p>
<p>
<img src="website/img/screenshots/phone/light/03-download-540.webp" alt="The recipient's download page on a phone" width="30%">
</p>
## Features
- **File Uploading** — Drag & drop or browse to upload single/multiple files and folders with real-time progress
- **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
- **End-to-End Encryption** — All files encrypted at rest using AES-256-GCM (chunked, streaming)
- **Encryption at Rest** — Files are encrypted on the server as they arrive, with AES-256-GCM (chunked, streaming); with a share password the key is derived from it and never stored. It is not end-to-end encryption: the server handles the files unencrypted while they are uploaded and downloaded
- **Password Protection** — Optionally protect shares with a password
- **Expiration** — Shares auto-expire after a configurable duration (1 hour to 30 days)
- **Download Limits** — Set a maximum number of downloads per share
+5
View File
@@ -84,6 +84,11 @@
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
],
"screenshots": [
"Composer\\Config::disableProcessTimeout",
"npm run build",
"@php -d upload_max_filesize=4G -d post_max_size=4G vendor/bin/pest tests/Screenshots"
]
},
"extra": {
+3
View File
@@ -8,6 +8,9 @@ services:
- "5173:5173"
volumes:
- .:/app
# Its own node_modules: npm installs the build tools' native binaries for Linux here and for
# the host's platform there, and a shared folder only ever holds one of them.
- /app/node_modules
environment:
APP_KEY: ${APP_KEY:-}
APP_URL: http://localhost:8000
+257
View File
@@ -0,0 +1,257 @@
# Screenshots and the SealShare website
## Goal
Two things that feed each other. First, one command — `composer screenshots` — produces every
screenshot of SealShare from fixed demo data, desktop and phone, light and dark, ready for the web.
Second, a static website at **sealshare.nonameweb.ch**, made the way mailifysms.nonameweb.ch is:
hand-written HTML and CSS in `website/`, uploaded by hand. The site presents SealShare as what it
is — software a company installs to run **its own upload platform**, so it exchanges files with
customers securely without relying on an outside service — shows the screenshots, compares
SealShare with hosted transfer services and with other self-hosted tools, and tells how to install
it. It goes live with 2.0.0. The README gets a few of the same screenshots, and its encryption
wording is corrected.
## Context
**MailifySMS, the model** (`../MailifySMS`):
- `website/` holds `index.html`, `privacy_policy.html`, `terms_and_conditions.html`,
`css/theme.css` (a palette sampled from the app's screenshots), `css/device-frame.css` (a phone
bezel shared with the store canvases), self-hosted Poppins (`fonts/`, OFL) and `img/`
(`icon.png`, `hero.jpg`, `screenshots/{light,dark}/NN-name.png` at 540px).
- `index.html`: sticky nav with a phone toggle, hero, "How it works", "Key features", a screenshot
gallery with a Light/Dark switch (`data-light`/`data-dark` on each `<img>`), FAQ accordion,
contact card (`surtic86@gmail.com`), footer (quick links, legal). Plausible:
`<script defer data-domain="mailifysms.nonameweb.ch" src="https://plausible.io/js/script.js">`.
The page's JS is one inline `<script>` at the end.
- `CLAUDE.md` records that `website/` "is a faithful copy of what is deployed, images included, so
it can be uploaded wholesale". There is no deploy automation.
- `tools/screenshots.sh` (macOS only) drives an emulator and headless Chrome; documented in
`CLAUDE.md` § Screenshots, with the reasons behind each quirk.
**Hosting.** `*.nonameweb.ch` is a wildcard DNS record to `80.74.140.2` (METANET shared hosting,
nginx), the same as mailifysms. `sealshare.nonameweb.ch` resolves already; HTTP serves the host's
placeholder, HTTPS has no certificate. Creating the site and its Let's Encrypt certificate is done
in the hosting panel.
**SealShare.**
- Laravel 13.31, Livewire 4.4, Livewire Material 1.0.1, Pest 5.1 with `pestphp/pest-plugin-browser`
(Playwright 1.63). The browser tests run the app in-process, so factories, `Storage::fake()` and
`travelTo()` shape what the browser sees. `tests/Pest.php` applies `Tests\TestCase` and
`RefreshDatabase` to `Feature` and `Browser`, and creates an admin in `beforeEach` (the setup
gate).
- Device presets: `visit()->on()->macbook14()` is 1512×982 at 2× (a 3024×1964 capture);
`on()->iPhone15Pro()` is 393×852 at 3× (1179×2556). `inLightMode()` / `inDarkMode()`,
`screenshot(fullPage, filename)`. Screenshots are written to `tests/Browser/Screenshots/<name>.png`;
the directory is created, subdirectories in the name are not — names must be flat. Pest empties
that directory when a browser run starts, and has no reduced-motion emulation.
- Pest's in-process server does not store a multipart upload, so a browser test cannot select files
through the file input.
- PHP here has GD with WebP and PNG support; the production image does not need it (this is a
development tool).
- Colours: `resources/css/material-scheme.json` (seed `#4f46e5`, Vibrant) holds the light and dark
roles as hexes. Font: Google Sans Flex, `vendor/nonameweb/livewire-material/resources/fonts/google-sans-flex/GoogleSansFlex-Latin.woff2`
with its `OFL.txt`. Logo: `resources/views/components/app-logo-icon.blade.php` (SVG).
- **Encryption, as the code does it:** `ShareService::createShare()` encrypts each uploaded file on
the server with AES-256-GCM (chunked) through `FileEncryptionService`. Without a share password
the key is stored in `shares.encryption_key`; with one, the key is derived with PBKDF2-SHA256 and
never stored. The server sees the plaintext while uploading and downloading. The README calls
this "End-to-End Encryption", which it is not.
- The upload page is public, optionally behind the system password (`SystemPasswordGate`); a
customer given that password can upload and send the link back.
- Install today (README): `ghcr.io/surtic86/sealshare`, clone from GitHub. Gitea
(`gitea.nonameweb.ch/noNameWEB/SealShare`) is now the public repository; the image registry for
2.0.0 is settled separately.
- `.gitignore` does not ignore `tests/Browser/Screenshots`; `.dockerignore` excludes `tests` and
`*.md` but would copy a `website/` directory into the image.
**Peers.** Pingvin Share has been archived since June 2025 (its README points to forks such as
Pingvin Share X). PsiTransfer, Gokapi and Erugo are single-purpose self-hosted share tools; Gokapi
advertises end-to-end encryption.
## Decisions
- **Positioning: software you host, not a service** — the site says plainly that SealShare is not
hosted by anyone but the company that installs it: its own upload platform for exchanging files
with customers, data on its own server, no dependence on an external service.
- **Pages: `index.html` and `privacy.html`** — one landing page, and a short privacy page because
the site uses Plausible. No terms page: the software is MIT-licensed and no service is offered.
- **A comparison with hosted transfer services and with self-hosted share tools** — two tables on
the landing page. Cloud suites (Nextcloud-style) are left out.
- **Hand-written HTML and CSS, like MailifySMS** — no build step; `website/` is uploaded as it is.
- **Colours copied from `material-scheme.json`, not sampled**`website/css/theme.css` lists the
roles it uses with the scheme's hexes, light by default and dark under
`@media (prefers-color-scheme: dark)`; it names the seed and variant it was copied from, so a
regenerated scheme is copied again. The site follows the visitor's system theme and has no
toggle of its own.
- **Google Sans Flex, self-hosted** — the app's font, copied with its `OFL.txt` into
`website/fonts/`; nothing is loaded from Google.
- **Uploaded by hand, like MailifySMS**`website/` is a faithful copy of what is live. You create
the subdomain and certificate once in the hosting panel and upload the folder when it changes.
No hosting credentials anywhere in the repository or CI.
- **Plausible**`data-domain="sealshare.nonameweb.ch"`, the same script as MailifySMS; the site
must be added in the Plausible account.
- **English only.**
- **Screenshots: desktop and phone, each in light and dark (20 images)**
desktop (MacBook 14, 2×): `01-upload` (files selected, options filled), `02-share-created`,
`03-qr-code` (the dialog), `04-download` (the recipient's file list), `05-admin-dashboard`,
`06-admin-settings`; phone (iPhone 15 Pro, 3×): `01-upload`, `02-password` (the recipient's
password prompt), `03-download`, `04-qr-code`.
- **Screenshots run as Pest browser tests in `tests/Screenshots/`, started by `composer screenshots`**
— the directory is not one of phpunit.xml's test suites, so `php artisan test`, the Browser
suite and CI never run it. It reuses Playwright and the in-process server.
- **Fixed demo data** — factories and `ShareService` with fixed names, sizes and tokens, time
frozen with `travelTo()`, the site title and branding at their defaults, one admin
("Alex Morgan"). Every run produces the same images unless the UI changed.
- **Images published as WebP by the test run itself** — after each capture a small helper resizes
it with GD into `website/img/screenshots/{desktop,phone}/{light,dark}/NN-name-<width>.webp` at two widths
(desktop 1600 and 800 px, phone 1080 and 540 px) for `srcset`. Raw PNGs stay in
`tests/Browser/Screenshots/`, which is gitignored; only the WebP files are committed.
- **Device frames in CSS**`website/css/device-frame.css` draws a laptop and a phone around the
screenshots; the hero shows the desktop upload and the phone download screenshots framed, in
the visitor's theme. No generated hero image.
- **README shows three screenshots** — desktop upload, desktop share created, phone download (light),
referenced from `website/img/screenshots/…`, so each image exists once in the repository.
- **Encryption is described accurately, on the site and in the README** — "encrypted at rest with
AES-256-GCM; with a share password the key is never stored". The comparison marks end-to-end
encryption "no" for SealShare. The README's "End-to-End Encryption" line is corrected.
- **Built on `material`, live with 2.0.0** — screenshots show the 2.0.0 interface; the site links
the Gitea repository, and its install commands are the README's at release, whatever registry
2.0.0 ships with.
- **Comparison facts are researched, dated and sourced** — from each product's own site,
documentation or repository; the tables say "as of <month year>" and link every source. Hosted
services: WeTransfer, SwissTransfer, Dropbox Transfer, Google Drive links. Self-hosted tools:
open source, single-purpose, installable with Docker, with a release in the 12 months before the
research — expected Pingvin Share X, PsiTransfer, Gokapi and Erugo; any that fails the rule is
dropped and named in the commit message. Criteria (rows): where files are stored, who operates
it, recipient needs an account, password protection, expiry, download limit, encryption at rest,
end-to-end encryption, folder upload, custom branding, maximum file size, licence and cost. A
value that cannot be sourced is "—", never guessed.
## Out of scope
- A terms page, a documentation section, a German version.
- Deploy automation (Gitea Actions, SFTP scripts), and creating the subdomain, certificate or
Plausible site — those are yours in the hosting panel and the Plausible account.
- Comparing with Nextcloud, ownCloud or other cloud suites.
- Store-style canvases with captions, a generated hero image, video or animated screenshots.
- Running the screenshots in CI, or checking them against earlier runs (visual regression).
- The image registry move and the install commands' final form (settled with 2.0.0).
- Implementing end-to-end encryption.
## Implementation steps
1. **Housekeeping.** `.gitignore`: `/tests/Browser/Screenshots`. `.dockerignore`: `website`.
2. **Screenshot helper.** `tests/Screenshots/Publisher.php` (`Tests\Screenshots\Publisher`):
`publish(string $capture, string $device, string $theme, string $name, array $widths): void`
reads `tests/Browser/Screenshots/<capture>.png` with GD, and for each width writes
`website/img/screenshots/<device>/<theme>/<name>-<width>.webp` (quality 82, aspect kept,
directories created). It throws when the capture is missing, so a failed shot fails the run.
3. **Demo data.** `tests/Screenshots/DemoData.php`: `admin()`, `shares()` (eight shares with fixed
tokens, file names such as `Q3-report.pdf`, `Contract 2026.pdf`, `Product photos/…`, sizes,
download counts and expiries, one password-protected, one expired), created through factories
and `ShareService` with `Storage::fake('shares')` so the files exist encrypted.
4. **Screenshot tests.** `tests/Screenshots/ScreenshotsTest.php`, with `tests/Pest.php` extended to
`->in('Feature', 'Browser', 'Screenshots')`:
- `beforeEach`: `config(['session.driver' => 'file'])`, `travelTo('2026-10-01 09:30')`, demo
data; a `ready()` wait as in `SealShareTest`.
- One test per device and theme (four tests), each visiting the pages in turn, waiting for
`networkidle` and fonts (`document.fonts.ready`), hiding the text caret, capturing
viewport-sized (not full-page) shots, and calling `Publisher::publish()` right after each.
- The upload shot with files selected: create Livewire temporary uploads on the fake
`livewire-tmp` disk and set the uploader's property through `$wire.$set` with
`livewire-file:` references, then fill the options. If Livewire refuses that, the shot shows
the drop zone with the options filled instead, and the plan's risk note is updated.
- QR dialog: `click('[data-test="show-qr-code"]')`; password prompt: the protected share on the
phone; admin pages as the admin.
5. **Command.** `composer.json` script `"screenshots"`: `Composer\\Config::disableProcessTimeout`,
`npm run build`, `@php vendor/bin/pest tests/Screenshots` — the build first, so the shots show
the current assets. Playwright's Chromium must be installed (`npx playwright install chromium`),
as for the browser tests.
6. **Website scaffold.** `website/`:
- `css/theme.css` — tokens copied from `material-scheme.json` (with seed and variant noted),
Google Sans Flex `@font-face`, layout, nav, hero, sections, cards, tables (scrolling
sideways on a phone), FAQ (`<details>`), footer; light and dark through
`prefers-color-scheme`.
- `css/device-frame.css` — laptop and phone frames.
- `fonts/GoogleSansFlex-Latin.woff2`, `fonts/OFL.txt`; `img/logo.svg` (from `app-logo-icon`),
`img/icon.png` (favicon, from `public/`).
7. **Landing page.** `website/index.html` (Plausible in `<head>`, one inline script at the end):
- nav: Why, Features, Screenshots, Compare, Install, FAQ, Gitea;
- hero: "Your own secure upload platform" — self-hosted file exchange with customers, no
outside service; buttons "Install" (to #install) and "Source on Gitea"; framed desktop and
phone screenshots as `<picture>` elements whose `<source media="(prefers-color-scheme: dark)">`
picks the dark captures;
- "Why run your own": your server, your domain and branding, customers upload and download
without accounts, encrypted at rest, no per-seat pricing;
- "How it works": upload → link or QR code → the customer downloads, with expiry, download
limit and password;
- features (from the README, accurate encryption wording);
- screenshots: Desktop/Phone and Light/Dark switches over one gallery (Light/Dark starting on
the visitor's system theme), `srcset` for both widths, `loading="lazy"`, descriptive `alt`;
- compare: the two dated tables with sources (step 8);
- install: the README's Docker quick start and a link to the full instructions on Gitea;
- FAQ: "Is it end-to-end encrypted?" (no — at rest, and what a password adds), "Can customers
send files to us?" (yes, through the upload page, optionally behind the system password),
"How big can files be?" (the README's large-file limits), "What does it cost?" (MIT, your
hosting), "Who runs it?" (you);
- contact (`surtic86@gmail.com`, as MailifySMS) and footer (Gitea, licence, privacy, noNameWEB).
8. **Comparison research.** For each product, record every criterion with its source URL and the
date checked; apply the self-hosted selection rule; fill the tables. Keep the notes in the
commit message, not in the repository.
9. **Privacy page.** `website/privacy.html`: who runs the site (contact), the host (METANET, server
logs), Plausible (cookieless, no personal data, EU-hosted, link to its data policy), no other
third parties, fonts served locally, contact for questions; dated.
10. **README and changelog.** Correct the encryption lines (the intro sentence stays accurate;
"End-to-End Encryption" becomes "Encryption at Rest", described as in Decisions), add a
Screenshots section with the three images, add the website link. CHANGELOG `2.0.0` "Fixed":
the README no longer calls the encryption end-to-end. The website and the screenshot tooling
get no changelog entry — they do not change the application.
11. **Project notes.** `record-rule` for `website/**`: `website/` is a faithful copy of what is live,
uploaded by hand; its colours are copied from `material-scheme.json` and must be copied again
when the scheme is regenerated; the comparison is dated and every value sourced. And for
`tests/Screenshots/**`: run with `composer screenshots` whenever the interface changes, before
a release; the demo data is fixed so runs are reproducible.
## Testing
- `tests/Unit/ScreenshotPublisherTest.php`: a generated PNG is written as WebP at each requested
width with the aspect ratio kept, into the device/theme directory; a missing capture throws.
- `tests/Feature/WebsiteTest.php` guards `website/` without a browser:
- every local `src`, `href`, `srcset` entry and CSS `url()` resolves to a file in `website/`;
- every screenshot the gallery or README references exists for both widths and both themes;
- no request goes to a host other than `plausible.io` (no Google Fonts, no CDN);
- `index.html` and `privacy.html` have a `<title>`, `lang="en"` and a meta description;
- the README has no "End-to-End Encryption" line, the site's features section does not say
"end-to-end", and SealShare's end-to-end cell in the comparison (marked
`data-compare="sealshare-e2e"`) reads "No" — the FAQ may still ask the question.
- The screenshot run itself is the test of step 4: it fails when a page, selector or capture
breaks. It is run by hand before a release, not in CI.
- The site is looked at in Chrome, Firefox and Safari, light and dark, at phone width, before
uploading.
## Risks and open questions
- **Selecting files in the upload shot** works: the files are stored with Livewire's own
`FileUploadConfiguration::storeTemporaryFile()` and handed to `_finishUpload` by their signed
names; Livewire's temporary-upload cleanup is turned off for the run, because under the frozen
clock it deletes them.
- **Comparison accuracy and fairness.** Other products change; the tables are dated and sourced,
and re-checked when the site is updated. Swiss unfair-competition law expects comparisons to be
accurate and not misleading — values that cannot be sourced stay "—".
- **Install commands depend on the registry move.** Until it is settled, the install section copies
the current README; it is updated before the site goes live with 2.0.0.
- **Screenshot determinism.** Relative dates ("in 3 days") depend on `travelTo()`; animations
(the share-created shape, counting stats, dialog entry) are waited out — Pest has no
reduced-motion emulation — by waiting on `document.getAnimations().length === 0` before each
capture.
- **The host's name in the privacy page** (METANET) is inferred from the server's reverse DNS
(`urbanus.ch-meta.net`); confirm it before the page goes live.
- **The contact address** is the one MailifySMS publishes (`surtic86@gmail.com`); change it in
step 7 if SealShare should have its own.
- **Image weight.** Twenty screenshots at two widths as WebP should stay under ~4 MB in total; if
not, lower the quality or drop the larger phone width.
- **Colours drift** when the scheme is regenerated; the rule in step 11 and the note in
`theme.css` are the guard.
+129
View File
@@ -0,0 +1,129 @@
<?php
/*
* website/ is uploaded by hand as it is, so these guard what a broken upload would show: files that
* are not there, requests to other hosts, and claims the application does not keep.
*/
/**
* The site's HTML pages, by name.
*
* @return array<string, string>
*/
function websitePages(): array
{
return [
'index.html' => file_get_contents(base_path('website/index.html')),
'privacy.html' => file_get_contents(base_path('website/privacy.html')),
];
}
/**
* Every URL an HTML document refers to in `src`, `href`, `srcset` and the gallery's `data-*-src` and
* `data-*-srcset`, with each srcset entry on its own.
*
* @return list<string>
*/
function websiteReferences(string $html): array
{
preg_match_all('/\s(src|href|srcset|data-[a-z]+-src|data-[a-z]+-srcset)="([^"]*)"/', $html, $matches, PREG_SET_ORDER);
return collect($matches)
->flatMap(fn (array $match): array => str_ends_with($match[1], 'srcset')
? array_map(fn (string $candidate): string => strtok(trim($candidate), ' '), explode(',', $match[2]))
: [$match[2]])
->map(fn (string $url): string => html_entity_decode($url))
->values()
->all();
}
test('every file the pages, their stylesheets and the README refer to exists', function () {
$missing = [];
foreach (websitePages() as $page => $html) {
foreach (websiteReferences($html) as $url) {
if (preg_match('/^(#|https?:|mailto:)/', $url) === 1) {
continue;
}
if (! is_file(base_path('website/'.strtok($url, '#?')))) {
$missing[] = "{$page}: {$url}";
}
}
}
foreach (glob(base_path('website/css/*.css')) as $stylesheet) {
preg_match_all('/url\([\'"]?([^\'")]+)[\'"]?\)/', file_get_contents($stylesheet), $matches);
foreach ($matches[1] as $url) {
if (! str_starts_with($url, 'data:') && ! is_file(dirname($stylesheet).'/'.strtok($url, '#?'))) {
$missing[] = basename($stylesheet).": {$url}";
}
}
}
foreach (websiteReferences(file_get_contents(base_path('README.md'))) as $url) {
if (str_starts_with($url, 'website/') && ! is_file(base_path($url))) {
$missing[] = "README.md: {$url}";
}
}
expect($missing)->toBe([]);
});
test('the screenshots are all there, for both themes and both widths', function () {
$expected = collect([
'desktop' => ['01-upload', '02-share-created', '03-qr-code', '04-download', '05-admin-dashboard', '06-admin-settings'],
'phone' => ['01-upload', '02-password', '03-download', '04-qr-code'],
])->flatMap(fn (array $names, string $device): array => collect(['light', 'dark'])
->crossJoin($names, $device === 'desktop' ? [1600, 800] : [1080, 540])
->map(fn (array $shot): string => "{$device}/{$shot[0]}/{$shot[1]}-{$shot[2]}.webp")
->all())
->sort()->values()->all();
$published = collect(glob(base_path('website/img/screenshots/*/*/*.webp')))
->map(fn (string $path): string => str($path)->after('website/img/screenshots/')->toString())
->sort()->values()->all();
expect($published)->toBe($expected);
});
test('the pages load nothing from another host but Plausible', function () {
foreach (websitePages() as $page => $html) {
preg_match_all('/<(?:script|img|source|link)\b[^>]*\s(?:src|srcset|href)="(https?:[^"]*)"/', $html, $matches);
expect(collect($matches[1])->reject(fn (string $url): bool => str_starts_with($url, 'https://plausible.io/'))->all())
->toBe([], "{$page} loads from another host");
}
foreach (glob(base_path('website/css/*.css')) as $stylesheet) {
expect(file_get_contents($stylesheet))->not->toMatch('/(url\(\s*[\'"]?https?:|@import)/');
}
});
test('the pages are titled, described and in English', function () {
foreach (websitePages() as $page => $html) {
expect($html)
->toContain('<html lang="en"')
->toMatch('/<title>[^<]+<\/title>/')
->toMatch('/<meta name="description" content="[^"]+"/')
->toContain('data-domain="sealshare.nonameweb.ch"');
}
});
test('neither the site nor the README calls the encryption end-to-end', function () {
$index = websitePages()['index.html'];
preg_match('/<section[^>]*id="features".*?<\/section>/s', $index, $features);
preg_match_all('/data-compare="sealshare-e2e">([^<]*)</', $index, $cells);
expect($features[0])->not->toMatch('/end-to-end/i')
->and($cells[1])->not->toBeEmpty()->each->toBe('No')
->and(file_get_contents(base_path('README.md')))->not->toContain('End-to-End Encryption');
});
test('nothing is left to fill in', function () {
foreach ([...websitePages(), 'theme.css' => file_get_contents(base_path('website/css/theme.css'))] as $file => $contents) {
expect(str_contains($contents, 'TODO'))->toBeFalse("{$file} still has a TODO");
}
});
+1 -1
View File
@@ -21,7 +21,7 @@ pest()->extend(TestCase::class)
// EnsureSetupComplete middleware redirects to /setup unless an admin exists.
User::factory()->admin()->create(['email' => 'admin-setup@test.com']);
})
->in('Feature', 'Browser');
->in('Feature', 'Browser', 'Screenshots');
/*
|--------------------------------------------------------------------------
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace Tests\Screenshots;
use App\Models\Share;
use App\Models\User;
use App\Services\ShareService;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
/**
* The shares and the admin every screenshot shows. Names, sizes, tokens, counts and dates are fixed
* (with the clock frozen by the caller), so a run only produces different images when the interface
* changed. Files go through ShareService, so they exist encrypted on the faked `shares` disk.
*/
final class DemoData
{
/** The share a customer receives: link, QR code and download page. */
public const DELIVERY_TOKEN = 'hG7kP2mXq9LrT4vB';
/** The password-protected share the phone's password prompt belongs to. */
public const PROTECTED_TOKEN = 'Wn3sD8fJ5cYa1ZeQ';
public const PROTECTED_PASSWORD = 'autumn-harbour-42';
public static function admin(): User
{
return User::factory()->admin()->create([
'name' => 'Alex Morgan',
'email' => 'alex.morgan@example.com',
]);
}
/**
* Eight shares of different ages, sizes and states: one expired, two password-protected, some
* with a download limit, some never expiring.
*/
public static function shares(): void
{
$shares = [
['token' => self::DELIVERY_TOKEN, 'daysAgo' => 0, 'expiresAfterDays' => 7, 'downloads' => 3, 'options' => [],
'files' => ['Q3 Report.pdf' => 2400, 'Contract 2026.pdf' => 380, 'Product photos/hero-shot.jpg' => 4800, 'Product photos/detail.jpg' => 3900]],
['token' => self::PROTECTED_TOKEN, 'daysAgo' => 1, 'expiresAfterDays' => 3, 'downloads' => 1, 'options' => ['password' => self::PROTECTED_PASSWORD, 'max_downloads' => 3],
'files' => ['Payroll September.xlsx' => 210, 'Tax statement 2025.pdf' => 640]],
['token' => 'Rt5uV6wX7yZ8aB9c', 'daysAgo' => 2, 'expiresAfterDays' => 30, 'downloads' => 7, 'options' => ['max_downloads' => 10],
'files' => ['Brand guidelines.pdf' => 8600]],
['token' => 'Kc2dE3fG4hJ5kL6m', 'daysAgo' => 5, 'expiresAfterDays' => 14, 'downloads' => 2, 'options' => [],
'files' => ['Site plan rev C.dwg' => 12400, 'Site plan rev C.pdf' => 3100]],
['token' => 'Np7qR8sT9uV1wX2y', 'daysAgo' => 9, 'expiresAfterDays' => null, 'downloads' => 0, 'options' => ['password' => 'demo-interview'],
'files' => ['Interview recording.m4a' => 18300]],
['token' => 'Zb4cD5eF6gH7jK8L', 'daysAgo' => 14, 'expiresAfterDays' => 30, 'downloads' => 0, 'options' => ['max_downloads' => 1],
'files' => ['Invoice 2026-0412.pdf' => 95]],
['token' => 'Mq1rS2tU3vW4xY5z', 'daysAgo' => 21, 'expiresAfterDays' => null, 'downloads' => 4, 'options' => [],
'files' => ['Onboarding/Welcome.pdf' => 520, 'Onboarding/Handbook.pdf' => 2900, 'Onboarding/IT checklist.docx' => 130]],
['token' => 'Ae6fG7hJ8kL9mN1p', 'daysAgo' => 40, 'expiresAfterDays' => 10, 'downloads' => 12, 'options' => [],
'files' => ['Event photos.zip' => 15700]],
];
foreach ($shares as $share) {
self::share(...$share);
}
}
/**
* @param array{password?: string, max_downloads?: int} $options
* @param array<string, int> $files relative path => size in kilobytes
*/
private static function share(string $token, int $daysAgo, ?int $expiresAfterDays, int $downloads, array $options, array $files): void
{
$createdAt = Carbon::now()->subDays($daysAgo)->subHours(2);
Str::createRandomStringsUsing(fn (): string => $token);
try {
$share = app(ShareService::class)->createShare(
collect($files)->map(fn (int $kilobytes, string $path): array => [
'file' => UploadedFile::fake()->create(basename($path), $kilobytes),
'relativePath' => str_contains($path, '/') ? $path : null,
])->values()->all(),
[...$options, 'expires_at' => $expiresAfterDays === null ? null : $createdAt->copy()->addDays($expiresAfterDays)],
);
} finally {
Str::createRandomStringsNormally();
}
$share->forceFill(['download_count' => $downloads, 'created_at' => $createdAt, 'updated_at' => $createdAt])->save();
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace Tests\Screenshots;
use GdImage;
use RuntimeException;
/**
* Turns a raw browser capture into the website's images: WebP at each width the site's `srcset`
* asks for, under `<device>/<theme>/<name>-<width>.webp`. The capture is read right after it is
* taken, because Pest empties its screenshot directory when a browser run starts.
*/
final class Publisher
{
public function __construct(
private string $capturesPath,
private string $publishPath,
private int $quality = 82,
) {}
/**
* The publisher for this project: Pest's captures into `website/img/screenshots`.
*/
public static function forProject(): self
{
return new self(base_path('tests/Browser/Screenshots'), base_path('website/img/screenshots'));
}
/**
* Write the capture as WebP at each width, keeping its aspect ratio.
*
* @param list<int> $widths
*/
public function publish(string $capture, string $device, string $theme, string $name, array $widths): void
{
$source = $this->capturesPath.'/'.$capture.'.png';
if (! is_file($source) || ! ($image = @imagecreatefrompng($source)) instanceof GdImage) {
throw new RuntimeException("The capture [{$capture}] was not written, so [{$device}/{$theme}/{$name}] cannot be published.");
}
$directory = $this->publishPath.'/'.$device.'/'.$theme;
if (! is_dir($directory)) {
mkdir($directory, 0755, true);
}
foreach ($widths as $width) {
$height = (int) round(imagesy($image) * $width / imagesx($image));
$resized = imagecreatetruecolor($width, $height);
imagecopyresampled($resized, $image, 0, 0, 0, 0, $width, $height, imagesx($image), imagesy($image));
imagewebp($resized, $directory.'/'.$name.'-'.$width.'.webp', $this->quality);
}
}
}
+153
View File
@@ -0,0 +1,153 @@
<?php
use App\Services\QrCodeService;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use Livewire\Features\SupportFileUploads\FileUploadConfiguration;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Tests\Screenshots\DemoData;
use Tests\Screenshots\Publisher;
/*
* The screenshots on the website and in the README, from fixed demo data with the clock frozen.
* Not part of any test suite: `composer screenshots` runs this directory (Chromium), and each capture
* is published to website/img/screenshots as soon as it is taken.
*/
beforeEach(function () {
// Sessions have to outlive a request: a sign-in, an unlocked share.
config(['session.driver' => 'file']);
Storage::fake('shares');
Storage::fake('tmp-for-tests');
$this->travelTo(Carbon::parse('2026-10-01 09:30'));
// Livewire deletes temporary uploads a day older than now, by the files' real modification
// times: under the frozen clock that is every file this run stores.
config(['livewire.temporary_file_upload.cleanup' => false]);
DemoData::shares();
});
/**
* A visited page on a device in a theme, once it can be used. Tests call `visit()` themselves, on a
* line of its own: Pest starts its browser only for tests under tests/Browser or whose body calls
* `visit(` after whitespace.
*/
function shotPage(mixed $visit, string $device, string $theme): mixed
{
$pending = $device === 'desktop' ? $visit->on()->macbook14() : $visit->on()->iPhone15Pro();
$page = $theme === 'dark' ? $pending->inDarkMode() : $pending->inLightMode();
return $page->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
}
/**
* Capture the viewport once fonts are in and every finite animation has run, then publish it.
*
* The in-process server listens on 127.0.0.1 at a random port, so links would read differently on
* every run: the page is shown as it reads on an installation at https://files.example.com, the
* server's origin replaced in text and fields, and the QR code drawn for that address.
*/
function shoot(mixed $page, string $device, string $theme, string $name): void
{
$origin = 'https://files.example.com';
$qrCode = app(QrCodeService::class)->svg($origin.'/s/'.DemoData::DELIVERY_TOKEN);
$page->script("document.head.insertAdjacentHTML('beforeend', '<style>*{caret-color:transparent!important}</style>')");
$page->script('(() => { const from = location.origin, to = '.json_encode($origin).'; document.querySelectorAll("input").forEach((input) => { input.value = input.value.replaceAll(from, to) }); const text = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); while (text.nextNode()) { text.currentNode.nodeValue = text.currentNode.nodeValue.replaceAll(from, to) } document.querySelectorAll("[data-qr-code]").forEach((panel) => { panel.innerHTML = '.json_encode($qrCode).' }) })()');
// Counting figures run on requestAnimationFrame rather than as Web Animations.
$page->wait(1.2)
->assertScript("document.fonts.status === 'loaded' && document.getAnimations().every((animation) => animation.playState !== 'running' || animation.effect?.getComputedTiming().iterations === Infinity)");
$capture = "{$device}-{$theme}-{$name}";
$page->screenshot(fullPage: false, filename: $capture);
Publisher::forProject()->publish($capture, $device, $theme, $name, $device === 'desktop' ? [1600, 800] : [1080, 540]);
}
/**
* Put files into the uploader the way a finished upload does: stored as Livewire temporary uploads
* and handed to `_finishUpload` by their signed names. A browser test cannot select files through
* the file input, because the in-process server does not store a multipart upload.
*
* @param array<string, int> $files relative path => size in kilobytes
*/
function selectFiles(mixed $page, array $files): void
{
$signed = [];
foreach ($files as $path => $kilobytes) {
// Livewire's own storage for an arriving upload: the file and its name, type and size beside it.
$stored = FileUploadConfiguration::storeTemporaryFile(UploadedFile::fake()->create(basename($path), $kilobytes), 'tmp-for-tests');
$signed[] = TemporaryUploadedFile::signPath(basename($stored));
}
$relativePaths = array_map(fn (string $path): ?string => str_contains($path, '/') ? $path : null, array_keys($files));
$page->script('(() => { const wire = Livewire.find(document.querySelector("[data-test=drop-zone]").closest("[wire\\\\:id]").getAttribute("wire:id")); wire.$set("relativePaths", '.json_encode($relativePaths).', false); wire._finishUpload("files", '.json_encode($signed).', true) })()');
$page->assertSee('Selected Files ('.count($files).')');
}
$files = ['Q3 Report.pdf' => 2400, 'Contract 2026.pdf' => 380, 'Product photos/hero-shot.jpg' => 4800, 'Product photos/detail.jpg' => 3900];
test('desktop', function (string $theme) use ($files) {
$upload = visit('/upload');
$page = shotPage($upload, 'desktop', $theme);
selectFiles($page, $files);
$page->click('label:has-text("Password protect")')
->type('input[wire\:model="password"]', DemoData::PROTECTED_PASSWORD)
->type('input[wire\:model="maxDownloads"]', '5');
// The drop zone, the files and the options fill the window; typing left the page wherever it scrolled.
$page->script("document.activeElement?.blur(); window.scrollTo(0, document.querySelector('[data-test=drop-zone]').getBoundingClientRect().top + window.scrollY - 24)");
shoot($page, 'desktop', $theme, '01-upload');
$created = visit(route('share.created', DemoData::DELIVERY_TOKEN, false));
$page = shotPage($created, 'desktop', $theme);
shoot($page, 'desktop', $theme, '02-share-created');
$page->click('[data-test="show-qr-code"]')
->assertScript("document.querySelector('[data-test=\"qr-code-dialog\"]').open");
shoot($page, 'desktop', $theme, '03-qr-code');
$download = visit(route('share.download', DemoData::DELIVERY_TOKEN, false));
shoot(shotPage($download, 'desktop', $theme), 'desktop', $theme, '04-download');
$this->actingAs(DemoData::admin());
$dashboard = visit('/admin/dashboard');
shoot(shotPage($dashboard, 'desktop', $theme), 'desktop', $theme, '05-admin-dashboard');
// Branding typed in but not saved, so the other shots keep SealShare's own.
$settings = visit('/admin/settings');
$page = shotPage($settings, 'desktop', $theme)
->type('input[wire\\:model="siteTitle"]', 'Acme Files')
->type('textarea[wire\\:model="siteDescription"]', 'Send and receive files securely with Acme Engineering.');
$page->script('document.activeElement?.blur(); window.scrollTo(0, 0)');
shoot($page, 'desktop', $theme, '06-admin-settings');
})->with(['light', 'dark']);
test('phone', function (string $theme) use ($files) {
$upload = visit('/upload');
$page = shotPage($upload, 'phone', $theme);
selectFiles($page, $files);
shoot($page, 'phone', $theme, '01-upload');
$password = visit(route('share.download', DemoData::PROTECTED_TOKEN, false));
shoot(shotPage($password, 'phone', $theme), 'phone', $theme, '02-password');
$download = visit(route('share.download', DemoData::DELIVERY_TOKEN, false));
shoot(shotPage($download, 'phone', $theme), 'phone', $theme, '03-download');
$created = visit(route('share.created', DemoData::DELIVERY_TOKEN, false));
$page = shotPage($created, 'phone', $theme);
$page->click('[data-test="show-qr-code"]')
->assertScript("document.querySelector('[data-test=\"qr-code-dialog\"]').open");
shoot($page, 'phone', $theme, '04-qr-code');
})->with(['light', 'dark']);
+32
View File
@@ -0,0 +1,32 @@
<?php
use Tests\Screenshots\Publisher;
beforeEach(function () {
$this->root = sys_get_temp_dir().'/sealshare-publisher-'.uniqid();
mkdir($this->root.'/captures', 0755, true);
});
afterEach(function () {
exec('rm -rf '.escapeshellarg($this->root));
});
test('a capture is published as WebP at each width, keeping its aspect ratio', function () {
$capture = imagecreatetruecolor(3024, 1964);
imagepng($capture, $this->root.'/captures/desktop-light-01-upload.png');
(new Publisher($this->root.'/captures', $this->root.'/site'))
->publish('desktop-light-01-upload', 'desktop', 'light', '01-upload', [1600, 800]);
foreach ([1600 => 1039, 800 => 520] as $width => $height) {
$path = $this->root."/site/desktop/light/01-upload-{$width}.webp";
expect($path)->toBeFile()
->and(getimagesize($path))->toMatchArray([0 => $width, 1 => $height, 'mime' => 'image/webp']);
}
});
test('a capture that was not written fails the run', function () {
(new Publisher($this->root.'/captures', $this->root.'/site'))
->publish('phone-dark-02-password', 'phone', 'dark', '02-password', [1080]);
})->throws(RuntimeException::class, 'The capture [phone-dark-02-password] was not written');
+115
View File
@@ -0,0 +1,115 @@
/*
* Device frames drawn around SealShare's screenshots: a laptop for the desktop captures
* (1512 × 982, a MacBook 14 viewport) and a phone for the phone captures (393 × 852, an iPhone 15
* Pro viewport). The screen keeps the capture's aspect ratio, so the frame holds its shape while an
* image loads. The screenshots come from `composer screenshots` (tests/Screenshots/).
*
* <div class="device device--laptop">
* <div class="device__screen"><picture><img ></picture></div>
* </div>
*/
.device {
--bezel: #1b1530;
--bezel-edge: #3a3355;
--deck: #c9c2dd;
--deck-shadow: #8f88a8;
position: relative;
width: 100%;
}
@media (prefers-color-scheme: dark) {
.device {
--bezel: #05020f;
--bezel-edge: #2a2244;
--deck: #4b426a;
--deck-shadow: #231649;
}
}
.device__screen {
position: relative;
overflow: hidden;
background: var(--surface-container-high, #e8deff);
}
.device__screen img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top center;
}
/* ---------------------------------------------------------------- laptop */
.device--laptop {
padding-bottom: 3.2%;
margin-inline: 4%;
width: auto;
}
.device--laptop .device__lid {
position: relative;
padding: 2.4% 2.4% 3%;
border-radius: 3.2% 3.2% 0 0 / 4.8% 4.8% 0 0;
background: var(--bezel);
box-shadow: inset 0 0 0 1px var(--bezel-edge), 0 24px 48px -24px rgb(16 6 45 / 0.45);
}
.device--laptop .device__screen {
aspect-ratio: 1512 / 982;
border-radius: 0.6%;
}
/* The camera notch at the top of the lid. */
.device--laptop .device__lid::before {
content: '';
position: absolute;
top: 0.9%;
left: 50%;
width: 1%;
aspect-ratio: 1;
border-radius: 50%;
background: var(--bezel-edge);
translate: -50% 0;
}
/* The deck: wider than the lid, with a thumb recess in the middle. */
.device--laptop::after {
content: '';
position: absolute;
left: -4%;
right: -4%;
bottom: 0;
height: 3.2%;
border-radius: 0 0 40% 40% / 0 0 100% 100%;
background: linear-gradient(var(--deck), var(--deck-shadow));
}
.device--laptop::before {
content: '';
position: absolute;
left: 50%;
bottom: 1.8%;
z-index: 1;
width: 14%;
height: 1.4%;
border-radius: 0 0 12px 12px;
background: var(--deck-shadow);
translate: -50% 0;
}
/* ---------------------------------------------------------------- phone */
.device--phone {
padding: 3.4%;
border-radius: 15% / 7%;
background: var(--bezel);
box-shadow: inset 0 0 0 2px var(--bezel-edge), 0 24px 48px -20px rgb(16 6 45 / 0.5);
}
.device--phone .device__screen {
aspect-ratio: 393 / 852;
border-radius: 12.5% / 5.8%;
}
File diff suppressed because it is too large Load Diff
Binary file not shown.
+91
View File
@@ -0,0 +1,91 @@
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" width="24" height="24">
<!-- Document outline with folded corner -->
<path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Z" fill="#6366f1" stroke="#4f46e5" stroke-width="0.5" />
<path d="M14 2v5a1 1 0 0 0 1 1h5" fill="#818cf8" stroke="#4f46e5" stroke-width="0.5" />
<!-- Upload arrow -->
<path d="M12 17v-6m0 0-2.5 2.5M12 11l2.5 2.5" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" width="96" height="96">
<!-- The SealShare mark: a document with a folded corner and an upload arrow (resources/views/components/app-logo-icon.blade.php). -->
<path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Z" stroke="#4a3fe2" stroke-width="1.5" />
<path d="M14 2v5a1 1 0 0 0 1 1h5" stroke="#4a3fe2" stroke-width="1.5" />
<path d="M12 17v-6m0 0-2.5 2.5M12 11l2.5 2.5" stroke="#4a3fe2" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+712
View File
@@ -0,0 +1,712 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SealShare — your own secure upload platform</title>
<meta name="description" content="SealShare is self-hosted file sharing: your company&#x27;s own upload platform to exchange files with customers, encrypted at rest, on your own server and domain. Open source, one Docker image.">
<meta name="theme-color" content="#faf4ff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#10062d" media="(prefers-color-scheme: dark)">
<meta property="og:type" content="website">
<meta property="og:url" content="https://sealshare.nonameweb.ch/">
<meta property="og:title" content="SealShare — your own secure upload platform">
<meta property="og:description" content="SealShare is self-hosted file sharing: your company&#x27;s own upload platform to exchange files with customers, encrypted at rest, on your own server and domain. Open source, one Docker image.">
<meta property="og:image" content="https://sealshare.nonameweb.ch/img/screenshots/desktop/light/01-upload-1600.webp">
<script defer data-domain="sealshare.nonameweb.ch" src="https://plausible.io/js/script.js"></script>
<link rel="icon" href="img/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="img/icon.png">
<link rel="preload" href="fonts/GoogleSansFlex-Latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="css/theme.css">
<link rel="stylesheet" href="css/device-frame.css">
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<svg xmlns="http://www.w3.org/2000/svg" class="sprite" aria-hidden="true">
<symbol id="i-cloud-upload" viewBox="0 -960 960 960"><path d="m440-446-36 35q-11 11-27.5 11T348-412q-11-11-11-28t11-28l104-104q12-12 28-12t28 12l104 104q11 11 11.5 27.5T612-412q-11 11-27.5 11.5T556-411l-36-35v206h220q42 0 71-29t29-71q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-83 0-141.5 58.5T280-520h-20q-58 0-99 41t-41 99q0 58 41 99t99 41h60q17 0 28.5 11.5T360-200q0 17-11.5 28.5T320-160h-60q-91 0-155.5-63T40-377q0-78 47-139t123-78q25-92 100-149t170-57q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 75-52.5 127.5T740-160H520q-33 0-56.5-23.5T440-240v-206Zm40 6Z"/></symbol>
<symbol id="i-link" viewBox="0 -960 960 960"><path d="M280-280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h120q17 0 28.5 11.5T440-640q0 17-11.5 28.5T400-600H280q-50 0-85 35t-35 85q0 50 35 85t85 35h120q17 0 28.5 11.5T440-320q0 17-11.5 28.5T400-280H280Zm80-160q-17 0-28.5-11.5T320-480q0-17 11.5-28.5T360-520h240q17 0 28.5 11.5T640-480q0 17-11.5 28.5T600-440H360Zm200 160q-17 0-28.5-11.5T520-320q0-17 11.5-28.5T560-360h120q50 0 85-35t35-85q0-50-35-85t-85-35H560q-17 0-28.5-11.5T520-640q0-17 11.5-28.5T560-680h120q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H560Z"/></symbol>
<symbol id="i-qr-code-2" viewBox="0 -960 960 960"><path d="M520-120v-80h80v80h-80Zm-80-80v-200h80v200h-80Zm320-120v-160h80v160h-80Zm-80-160v-80h80v80h-80Zm-480 80v-80h80v80h-80Zm-80-80v-80h80v80h-80Zm360-280v-80h80v80h-80ZM180-660h120v-120H180v120Zm-60 20v-160q0-17 11.5-28.5T160-840h160q17 0 28.5 11.5T360-800v160q0 17-11.5 28.5T320-600H160q-17 0-28.5-11.5T120-640Zm60 460h120v-120H180v120Zm-60 20v-160q0-17 11.5-28.5T160-360h160q17 0 28.5 11.5T360-320v160q0 17-11.5 28.5T320-120H160q-17 0-28.5-11.5T120-160Zm540-500h120v-120H660v120Zm-60 20v-160q0-17 11.5-28.5T640-840h160q17 0 28.5 11.5T840-800v160q0 17-11.5 28.5T800-600H640q-17 0-28.5-11.5T600-640Zm80 520v-120h-80v-80h160v120h80v80H680ZM520-400v-80h160v80H520Zm-160 0v-80h-80v-80h240v80h-80v80h-80Zm40-200v-160h80v80h80v80H400Zm-190-90v-60h60v60h-60Zm0 480v-60h60v60h-60Zm480-480v-60h60v60h-60Z"/></symbol>
<symbol id="i-lock" viewBox="0 -960 960 960"><path d="M240-80q-33 0-56.5-23.5T160-160v-400q0-33 23.5-56.5T240-640h40v-80q0-83 58.5-141.5T480-920q83 0 141.5 58.5T680-720v80h40q33 0 56.5 23.5T800-560v400q0 33-23.5 56.5T720-80H240Zm0-80h480v-400H240v400Zm240-120q33 0 56.5-23.5T560-360q0-33-23.5-56.5T480-440q-33 0-56.5 23.5T400-360q0 33 23.5 56.5T480-280ZM360-640h240v-80q0-50-35-85t-85-35q-50 0-85 35t-35 85v80ZM240-160v-400 400Z"/></symbol>
<symbol id="i-schedule" viewBox="0 -960 960 960"><path d="M520-496v-144q0-17-11.5-28.5T480-680q-17 0-28.5 11.5T440-640v159q0 8 3 15.5t9 13.5l132 132q11 11 28 11t28-11q11-11 11-28t-11-28L520-496ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-400Zm0 320q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Z"/></symbol>
<symbol id="i-download" viewBox="0 -960 960 960"><path d="M480-337q-8 0-15-2.5t-13-8.5L308-492q-12-12-11.5-28t11.5-28q12-12 28.5-12.5T365-549l75 75v-286q0-17 11.5-28.5T480-800q17 0 28.5 11.5T520-760v286l75-75q12-12 28.5-11.5T652-548q11 12 11.5 28T652-492L508-348q-6 6-13 8.5t-15 2.5ZM240-160q-33 0-56.5-23.5T160-240v-80q0-17 11.5-28.5T200-360q17 0 28.5 11.5T240-320v80h480v-80q0-17 11.5-28.5T760-360q17 0 28.5 11.5T800-320v80q0 33-23.5 56.5T720-160H240Z"/></symbol>
<symbol id="i-folder-zip" viewBox="0 -960 960 960"><path d="M640-480v-80h80v80h-80Zm0 80h-80v-80h80v80Zm0 80v-80h80v80h-80ZM447-640l-80-80H160v480h400v-80h80v80h160v-400H640v80h-80v-80H447ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h207q16 0 30.5 6t25.5 17l57 57h320q33 0 56.5 23.5T880-640v400q0 33-23.5 56.5T800-160H160Zm0-80v-480 480Z"/></symbol>
<symbol id="i-delete-sweep" viewBox="0 -960 960 960"><path d="M200-200q-33 0-56.5-23.5T120-280v-360q-17 0-28.5-11.5T80-680q0-17 11.5-28.5T120-720h120v-20q0-17 11.5-28.5T280-780h80q17 0 28.5 11.5T400-740v20h120q17 0 28.5 11.5T560-680q0 17-11.5 28.5T520-640v360q0 33-23.5 56.5T440-200H200Zm440-40q-17 0-28.5-11.5T600-280q0-17 11.5-28.5T640-320h80q17 0 28.5 11.5T760-280q0 17-11.5 28.5T720-240h-80Zm0-160q-17 0-28.5-11.5T600-440q0-17 11.5-28.5T640-480h160q17 0 28.5 11.5T840-440q0 17-11.5 28.5T800-400H640Zm0-160q-17 0-28.5-11.5T600-600q0-17 11.5-28.5T640-640h200q17 0 28.5 11.5T880-600q0 17-11.5 28.5T840-560H640Zm-440-80v360h240v-360H200Z"/></symbol>
<symbol id="i-admin-panel-settings" viewBox="0 -960 960 960"><path d="M680-280q25 0 42.5-17.5T740-340q0-25-17.5-42.5T680-400q-25 0-42.5 17.5T620-340q0 25 17.5 42.5T680-280Zm0 120q31 0 57-14.5t42-38.5q-22-13-47-20t-52-7q-27 0-52 7t-47 20q16 24 42 38.5t57 14.5ZM480-874q8 0 14.5 1t13.5 4l240 90q23 9 37.5 29t14.5 45v125q0 17-11.5 28.5T760-540q-17 0-28.5-11.5T720-580v-124l-240-90-240 90v188q0 50 14.5 100t40 95q25.5 45 62 81t79.5 59q15 8 21.5 23t.5 30q-7 16-22.5 22t-30.5-2q-113-56-179-169t-66-239v-189q0-25 14.5-45t37.5-29l240-90q7-3 14-4t14-1ZM680-80q-83 0-141.5-58.5T480-280q0-83 58.5-141.5T680-480q83 0 141.5 58.5T880-280q0 83-58.5 141.5T680-80ZM480-494Z"/></symbol>
<symbol id="i-palette" viewBox="0 -960 960 960"><path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 32.5-156t88-127Q256-817 330-848.5T488-880q80 0 151 27.5t124.5 76q53.5 48.5 85 115T880-518q0 115-70 176.5T640-280h-74q-9 0-12.5 5t-3.5 11q0 12 15 34.5t15 51.5q0 50-27.5 74T480-80Zm0-400Zm-220 40q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm120-160q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm200 0q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm120 160q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17ZM480-160q9 0 14.5-5t5.5-13q0-14-15-33t-15-57q0-42 29-67t71-25h70q66 0 113-38.5T800-518q0-121-92.5-201.5T488-800q-136 0-232 93t-96 227q0 133 93.5 226.5T480-160Z"/></symbol>
<symbol id="i-shield-lock" viewBox="0 -960 960 960"><path d="M400-320h160q17 0 28.5-11.5T600-360v-120q0-17-11.5-28.5T560-520v-40q0-33-23.5-56.5T480-640q-33 0-56.5 23.5T400-560v40q-17 0-28.5 11.5T360-480v120q0 17 11.5 28.5T400-320Zm40-200v-40q0-17 11.5-28.5T480-600q17 0 28.5 11.5T520-560v40h-80Zm40 436q-7 0-13-1t-12-3q-135-45-215-166.5T160-516v-189q0-25 14.5-45t37.5-29l240-90q14-5 28-5t28 5l240 90q23 9 37.5 29t14.5 45v189q0 140-80 261.5T505-88q-6 2-12 3t-13 1Zm0-80q104-33 172-132t68-220v-189l-240-90-240 90v189q0 121 68 220t172 132Zm0-316Z"/></symbol>
<symbol id="i-phonelink-lock" viewBox="0 -960 960 960"><path d="M634-320q-14 0-24-10t-10-24v-132q0-14 10-24t24-10h6v-40q0-33 23.5-56.5T720-640q33 0 56.5 23.5T800-560v40h6q14 0 24 10t10 24v132q0 14-10 24t-24 10H634Zm46-200h80v-40q0-17-11.5-28.5T720-600q-17 0-28.5 11.5T680-560v40ZM280-40q-33 0-56.5-23.5T200-120v-720q0-33 23.5-56.5T280-920h400q33 0 56.5 23.5T760-840v120q0 17-11.5 28.5T720-680q-17 0-28.5-11.5T680-720H280v480h400q0-17 11.5-28.5T720-280q17 0 28.5 11.5T760-240v120q0 33-23.5 56.5T680-40H280Zm0-120v40h400v-40H280Zm0-640h400v-40H280v40Zm0 0v-40 40Zm0 640v40-40Z"/></symbol>
<symbol id="i-deployed-code" viewBox="0 -960 960 960"><path d="M440-183v-274L200-596v274l240 139Zm80 0 240-139v-274L520-457v274Zm-40-343 237-137-237-137-237 137 237 137ZM160-252q-19-11-29.5-29T120-321v-318q0-22 10.5-40t29.5-29l280-161q19-11 40-11t40 11l280 161q19 11 29.5 29t10.5 40v318q0 22-10.5 40T800-252L520-91q-19 11-40 11t-40-11L160-252Zm320-228Z"/></symbol>
<symbol id="i-dns" viewBox="0 -960 960 960"><path d="M300-720q-25 0-42.5 17.5T240-660q0 25 17.5 42.5T300-600q25 0 42.5-17.5T360-660q0-25-17.5-42.5T300-720Zm0 400q-25 0-42.5 17.5T240-260q0 25 17.5 42.5T300-200q25 0 42.5-17.5T360-260q0-25-17.5-42.5T300-320ZM160-840h640q17 0 28.5 11.5T840-800v280q0 17-11.5 28.5T800-480H160q-17 0-28.5-11.5T120-520v-280q0-17 11.5-28.5T160-840Zm40 80v200h560v-200H200Zm-40 320h640q17 0 28.5 11.5T840-400v280q0 17-11.5 28.5T800-80H160q-17 0-28.5-11.5T120-120v-280q0-17 11.5-28.5T160-440Zm40 80v200h560v-200H200Zm0-400v200-200Zm0 400v200-200Z"/></symbol>
<symbol id="i-no-accounts" viewBox="0 -960 960 960"><path d="M608-522 422-708q14-6 28.5-9t29.5-3q59 0 99.5 40.5T620-580q0 15-3 29.5t-9 28.5ZM234-276q51-39 114-61.5T480-360q18 0 34.5 1.5T549-354l-88-88q-47-6-80.5-39.5T341-562L227-676q-32 41-49.5 90.5T160-480q0 59 19.5 111t54.5 93Zm498-8q32-41 50-90.5T800-480q0-133-93.5-226.5T480-800q-56 0-105.5 18T284-732l448 448ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Zm0-80q53 0 100-15.5t86-44.5q-39-29-86-44.5T480-280q-53 0-100 15.5T294-220q39 29 86 44.5T480-160Zm0-60Z"/></symbol>
<symbol id="i-money-off" viewBox="0 -960 960 960"><path d="M440-756v-44q0-17 11.5-28.5T480-840q17 0 28.5 11.5T520-800v42q30 6 56.5 21.5T623-698q11 14 9.5 30.5T617-640q-14 11-30.5 9.5T559-646q-14-17-34-26.5t-43-9.5q-11 0-21.5 3t-20.5 8q-16 7-31.5 1T386-692q-7-16-1-31.5t22-22.5q8-4 16.5-6t16.5-4ZM764-84 608-240q-15 15-41 24.5T520-204v44q0 17-11.5 28.5T480-120q-17 0-28.5-11.5T440-160v-46q-44-11-77-36.5T310-306q-8-16-3-33t21-25q17-8 33.5-2t25.5 22q15 28 40.5 46t60.5 18q18 0 33-4.5t29-13.5L84-764q-11-11-11-28t11-28q11-11 28-11t28 11l680 680q11 11 11 28t-11 28q-11 11-28 11t-28-11Z"/></symbol>
<symbol id="i-drive-folder-upload" viewBox="0 -960 960 960"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h207q16 0 30.5 6t25.5 17l57 57h320q33 0 56.5 23.5T880-640v400q0 33-23.5 56.5T800-160H160Zm0-80h640v-400H447l-80-80H160v480Zm0 0v-480 480Zm320-40q17 0 28.5-11.5T520-320v-128l36 36q11 11 28 11t28-11q11-11 11-28t-11-28L508-572q-12-12-28-12t-28 12L348-468q-11 11-11 28t11 28q11 11 28 11t28-11l36-36v128q0 17 11.5 28.5T480-280Z"/></symbol>
<symbol id="i-counter-5" viewBox="0 -960 960 960"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Zm-80 200h120q33 0 56.5-23.5T600-360v-80q0-33-23.5-56.5T520-520h-80v-80h120q17 0 28.5-11.5T600-640q0-17-11.5-28.5T560-680H400q-17 0-28.5 11.5T360-640v160q0 17 11.5 28.5T400-440h120v80H400q-17 0-28.5 11.5T360-320q0 17 11.5 28.5T400-280Z"/></symbol>
<symbol id="i-check" viewBox="0 -960 960 960"><path d="m382-354 339-339q12-12 28-12t28 12q12 12 12 28.5T777-636L410-268q-12 12-28 12t-28-12L182-440q-12-12-11.5-28.5T183-497q12-12 28.5-12t28.5 12l142 143Z"/></symbol>
<symbol id="i-close" viewBox="0 -960 960 960"><path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/></symbol>
<symbol id="i-menu" viewBox="0 -960 960 960"><path d="M160-240q-17 0-28.5-11.5T120-280q0-17 11.5-28.5T160-320h640q17 0 28.5 11.5T840-280q0 17-11.5 28.5T800-240H160Zm0-200q-17 0-28.5-11.5T120-480q0-17 11.5-28.5T160-520h640q17 0 28.5 11.5T840-480q0 17-11.5 28.5T800-440H160Zm0-200q-17 0-28.5-11.5T120-680q0-17 11.5-28.5T160-720h640q17 0 28.5 11.5T840-680q0 17-11.5 28.5T800-640H160Z"/></symbol>
<symbol id="i-mail" viewBox="0 -960 960 960"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm640-480L501-453q-5 3-10.5 4.5T480-447q-5 0-10.5-1.5T459-453L160-640v400h640v-400ZM480-520l320-200H160l320 200ZM160-640v10-59 1-32 32-.5 58.5-10 400-400Z"/></symbol>
<symbol id="i-arrow-forward" viewBox="0 -960 960 960"><path d="M647-440H200q-17 0-28.5-11.5T160-480q0-17 11.5-28.5T200-520h447L451-716q-12-12-11.5-28t12.5-28q12-11 28-11.5t28 11.5l264 264q6 6 8.5 13t2.5 15q0 8-2.5 15t-8.5 13L508-188q-11 11-27.5 11T452-188q-12-12-12-28.5t12-28.5l195-195Z"/></symbol>
<symbol id="i-open-in-new" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h240q17 0 28.5 11.5T480-800q0 17-11.5 28.5T440-760H200v560h560v-240q0-17 11.5-28.5T800-480q17 0 28.5 11.5T840-440v240q0 33-23.5 56.5T760-120H200Zm560-584L416-360q-11 11-28 11t-28-11q-11-11-11-28t11-28l344-344H600q-17 0-28.5-11.5T560-800q0-17 11.5-28.5T600-840h200q17 0 28.5 11.5T840-800v200q0 17-11.5 28.5T800-560q-17 0-28.5-11.5T760-600v-104Z"/></symbol>
<symbol id="i-terminal" viewBox="0 -960 960 960"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-400H160v400Zm187-200-76-76q-12-12-11.5-28t12.5-28q12-11 28-11.5t28 11.5l104 104q12 12 12 28t-12 28L328-308q-11 11-27.5 11.5T272-308q-11-11-11-28t11-28l75-76Zm173 160q-17 0-28.5-11.5T480-320q0-17 11.5-28.5T520-360h160q17 0 28.5 11.5T720-320q0 17-11.5 28.5T680-280H520Z"/></symbol>
<symbol id="i-expand-more" viewBox="0 -960 960 960"><path d="M480-362q-8 0-15-2.5t-13-8.5L268-557q-11-11-11-28t11-28q11-11 28-11t28 11l156 156 156-156q11-11 28-11t28 11q11 11 11 28t-11 28L508-373q-6 6-13 8.5t-15 2.5Z"/></symbol>
<symbol id="i-encrypted" viewBox="0 -960 960 960"><path d="M444-360h72q9 0 15.5-7.5T536-384l-19-105q20-10 31.5-29t11.5-42q0-33-23.5-56.5T480-640q-33 0-56.5 23.5T400-560q0 23 11.5 42t31.5 29l-19 105q-2 9 4.5 16.5T444-360Zm36 276q-7 0-13-1t-12-3q-135-45-215-166.5T160-516v-189q0-25 14.5-45t37.5-29l240-90q14-5 28-5t28 5l240 90q23 9 37.5 29t14.5 45v189q0 140-80 261.5T505-88q-6 2-12 3t-13 1Zm0-80q104-33 172-132t68-220v-189l-240-90-240 90v189q0 121 68 220t172 132Zm0-316Z"/></symbol>
<symbol id="i-dashboard" viewBox="0 -960 960 960"><path d="M520-640v-160q0-17 11.5-28.5T560-840h240q17 0 28.5 11.5T840-800v160q0 17-11.5 28.5T800-600H560q-17 0-28.5-11.5T520-640ZM120-480v-320q0-17 11.5-28.5T160-840h240q17 0 28.5 11.5T440-800v320q0 17-11.5 28.5T400-440H160q-17 0-28.5-11.5T120-480Zm400 320v-320q0-17 11.5-28.5T560-520h240q17 0 28.5 11.5T840-480v320q0 17-11.5 28.5T800-120H560q-17 0-28.5-11.5T520-160Zm-400 0v-160q0-17 11.5-28.5T160-360h240q17 0 28.5 11.5T440-320v160q0 17-11.5 28.5T400-120H160q-17 0-28.5-11.5T120-160Zm80-360h160v-240H200v240Zm400 320h160v-240H600v240Zm0-480h160v-80H600v80ZM200-200h160v-80H200v80Zm160-320Zm240-160Zm0 240ZM360-280Z"/></symbol>
<symbol id="i-bug-report" viewBox="0 -960 960 960"><path d="M480-120q-65 0-120.5-32T272-240h-72q-17 0-28.5-11.5T160-280q0-17 11.5-28.5T200-320h44q-3-20-3.5-40t-.5-40h-40q-17 0-28.5-11.5T160-440q0-17 11.5-28.5T200-480h40q0-20 .5-40t3.5-40h-44q-17 0-28.5-11.5T160-600q0-17 11.5-28.5T200-640h72q14-23 31.5-43t40.5-35l-37-38q-11-11-11-27.5t12-28.5q11-11 28-11t28 11l58 58q28-9 57-9t57 9l60-59q11-11 27.5-11t28.5 12q11 11 11 28t-11 28l-38 38q23 15 41.5 34.5T688-640h72q17 0 28.5 11.5T800-600q0 17-11.5 28.5T760-560h-44q3 20 3.5 40t.5 40h40q17 0 28.5 11.5T800-440q0 17-11.5 28.5T760-400h-40q0 20-.5 40t-3.5 40h44q17 0 28.5 11.5T800-280q0 17-11.5 28.5T760-240h-72q-32 56-87.5 88T480-120Zm0-80q66 0 113-47t47-113v-160q0-66-47-113t-113-47q-66 0-113 47t-47 113v160q0 66 47 113t113 47Zm-40-120h80q17 0 28.5-11.5T560-360q0-17-11.5-28.5T520-400h-80q-17 0-28.5 11.5T400-360q0 17 11.5 28.5T440-320Zm0-160h80q17 0 28.5-11.5T560-520q0-17-11.5-28.5T520-560h-80q-17 0-28.5 11.5T400-520q0 17 11.5 28.5T440-480Zm40 40Z"/></symbol>
<symbol id="s-cookie-9" viewBox="0 0 100 100"><path d="M50 2.5C53.6 2.5 57.3 3.8 60.2 6.4C63.1 9.1 66.9 10.5 70.9 10.3C78.7 10 85.4 15.7 86.4 23.4C87 27.3 89 30.8 92.1 33.3C98.3 38 99.8 46.7 95.7 53.3C93.6 56.6 92.9 60.6 93.7 64.5C95.3 72.1 90.9 79.7 83.5 82.1C79.8 83.3 76.6 85.9 74.8 89.4C71.2 96.3 62.9 99.3 55.7 96.4C52.1 94.9 47.9 94.9 44.3 96.4C37.1 99.3 28.8 96.3 25.2 89.4C23.4 85.9 20.2 83.3 16.5 82.1C9.1 79.7 4.7 72.1 6.3 64.5C7.1 60.6 6.4 56.6 4.3 53.3C0.2 46.7 1.7 38 7.9 33.3C11 30.8 13 27.3 13.6 23.4C14.6 15.7 21.3 10 29.1 10.3C33.1 10.5 36.9 9.1 39.8 6.4C42.7 3.8 46.4 2.5 50 2.5Z"/></symbol>
<symbol id="s-soft-burst" viewBox="0 0 100 100"><path d="M19.4 27.8C20.1 26.8 20.5 25.7 20.4 24.4C20.1 21.5 19.9 18.6 19.7 15.7C19.4 11.9 23.2 9.2 26.6 10.6C29.4 11.7 32.1 12.8 34.8 13.9C37.1 14.9 39.8 14 41 11.9C42.6 9.4 44.1 6.9 45.7 4.4C47.6 1.2 52.3 1.2 54.3 4.4C55.8 6.9 57.4 9.4 58.9 11.9C60.2 14 62.9 14.9 65.2 13.9C67.9 12.8 70.6 11.7 73.3 10.6C76.8 9.1 80.5 11.9 80.3 15.6C80 18.5 79.8 21.5 79.6 24.4C79.4 26.9 81.1 29.1 83.5 29.7C86.3 30.4 89.2 31.1 92 31.8C95.7 32.7 97.1 37.1 94.7 40C92.8 42.2 90.9 44.5 89 46.7C87.4 48.6 87.4 51.4 89 53.3C90.9 55.5 92.8 57.7 94.7 60C97.1 62.8 95.7 67.3 92.1 68.2C89.2 68.9 86.3 69.6 83.5 70.3C81.1 70.9 79.5 73.1 79.6 75.6C79.9 78.5 80.1 81.4 80.3 84.3C80.6 88.1 76.8 90.8 73.4 89.4C70.6 88.3 67.9 87.2 65.2 86.1C62.9 85.1 60.2 86 59 88.1C57.4 90.6 55.9 93.1 54.3 95.6C52.4 98.8 47.7 98.8 45.7 95.6C44.2 93.1 42.6 90.6 41.1 88.1C39.8 86 37.1 85.1 34.8 86.1C32.1 87.2 29.4 88.3 26.7 89.4C23.2 90.9 19.5 88.1 19.7 84.4C20 81.5 20.2 78.5 20.4 75.6C20.6 73.1 18.9 70.9 16.5 70.3C13.7 69.6 10.8 68.9 8 68.2C4.3 67.3 2.9 62.9 5.3 60C7.2 57.8 9.1 55.5 11 53.3C12.6 51.4 12.6 48.6 11 46.7C9.1 44.5 7.2 42.3 5.3 40C2.9 37.2 4.3 32.7 7.9 31.8C10.8 31.1 13.7 30.4 16.5 29.7C17.7 29.4 18.7 28.7 19.4 27.8Z"/></symbol>
<symbol id="s-clover-4" viewBox="0 0 100 100"><path d="M50 9.1C50.5 8.7 51 8.4 51.5 8C56.7 4 62.9 2 69.1 2C76.5 2 83.9 4.9 89.5 10.5C95.1 16.1 98 23.5 98 30.9C98 37.1 96 43.3 92 48.5C91.6 49 91.3 49.5 90.9 50C91.3 50.5 91.6 51 92 51.5C96 56.7 98 62.9 98 69.1C98 76.5 95.1 83.9 89.5 89.5C83.9 95.1 76.5 98 69.1 98C62.9 98 56.7 96 51.5 92C51 91.6 50.5 91.3 50 90.9C49.5 91.3 49 91.6 48.5 92C43.3 96 37.1 98 30.9 98C23.5 98 16.1 95.1 10.5 89.5C4.9 83.9 2 76.5 2 69.1C2 62.9 4 56.7 8 51.5C8.4 51 8.7 50.5 9.1 50C8.7 49.5 8.4 49 8 48.5C4 43.3 2 37.1 2 30.9C2 23.5 4.9 16.1 10.5 10.5C16.1 4.9 23.5 2 30.9 2C37.1 2 43.3 4 48.5 8C49 8.4 49.5 8.7 50 9.1Z"/></symbol>
<symbol id="s-puffy-diamond" viewBox="0 0 100 100"><path d="M76.8 23.2C80 26.4 81.7 31.2 80.5 36.3C81 36.2 81.5 36.1 82 36.1C90.5 34.9 98 41.5 98 50C98 58.5 90.5 65.1 82 63.9C81.5 63.9 81 63.8 80.5 63.7C82.8 73.8 73.8 82.8 63.7 80.5C63.8 81 63.9 81.5 63.9 82C65.1 90.5 58.5 98 50 98C41.5 98 34.9 90.5 36.1 82C36.1 81.5 36.2 81 36.3 80.5C26.2 82.8 17.2 73.8 19.5 63.7C19 63.8 18.5 63.9 18 63.9C9.5 65.1 2 58.5 2 50C2 41.5 9.5 34.9 18 36.1C18.5 36.1 19 36.2 19.5 36.3C17.2 26.2 26.2 17.2 36.3 19.5C36.2 19 36.1 18.5 36.1 18C34.9 9.5 41.5 2 50 2C58.5 2 65.1 9.5 63.9 18C63.9 18.5 63.8 19 63.7 19.5C68.8 18.3 73.6 20 76.8 23.2Z"/></symbol>
<symbol id="s-sunny" viewBox="0 0 100 100"><path d="M98 50C98 51.7 97.4 53.5 96.2 54.9C93.7 57.8 91.3 60.7 88.8 63.6C87.8 64.8 87.2 66.3 87 67.9C86.7 71.6 86.4 75.4 86.1 79.2C85.8 82.9 82.9 85.8 79.2 86.1C75.4 86.4 71.6 86.7 67.9 87C66.3 87.2 64.8 87.8 63.6 88.8C60.7 91.3 57.8 93.7 54.9 96.2C52.1 98.6 47.9 98.6 45.1 96.2C42.2 93.7 39.3 91.3 36.4 88.8C35.2 87.8 33.7 87.2 32.1 87C28.4 86.7 24.6 86.4 20.8 86.1C17.1 85.8 14.2 82.9 13.9 79.2C13.6 75.4 13.3 71.6 13 67.9C12.8 66.3 12.2 64.8 11.2 63.6C8.7 60.7 6.3 57.8 3.8 54.9C1.4 52.1 1.4 47.9 3.8 45.1C6.3 42.2 8.7 39.3 11.2 36.4C12.2 35.2 12.8 33.7 13 32.1C13.3 28.4 13.6 24.6 13.9 20.8C14.2 17.1 17.1 14.2 20.8 13.9C24.6 13.6 28.4 13.3 32.1 13C33.7 12.8 35.2 12.2 36.4 11.2C39.3 8.7 42.2 6.3 45.1 3.8C47.9 1.4 52.1 1.4 54.9 3.8C57.8 6.3 60.7 8.7 63.6 11.2C64.8 12.2 66.3 12.8 67.9 13C71.6 13.3 75.4 13.6 79.2 13.9C82.9 14.2 85.8 17.1 86.1 20.8C86.4 24.6 86.7 28.4 87 32.1C87.2 33.7 87.8 35.2 88.8 36.4C91.3 39.3 93.7 42.2 96.2 45.1C97.4 46.5 98 48.3 98 50Z"/></symbol>
<symbol id="s-cookie-6" viewBox="0 0 100 100"><path d="M71.8 87.5C70.1 88.5 68.5 89.7 67.1 91C67 91.1 67 91.1 66.9 91.2C62.2 95.7 56.1 98 50 98C43.9 98 37.8 95.7 33.1 91.2C30.3 88.4 26.8 86.4 23 85.3C22.9 85.3 22.9 85.3 22.8 85.3C10.2 81.6 2.7 68.7 5.9 55.9C6.9 52.1 6.9 48.1 5.9 44.3C5.9 44.2 5.9 44.1 5.9 44.1C2.7 31.3 10.2 18.4 22.8 14.8C26.6 13.7 30.1 11.7 32.9 9C33 8.9 33 8.9 33.1 8.8C37.8 4.3 43.9 2 50 2C56.1 2 62.2 4.3 66.9 8.8C69.7 11.6 73.2 13.6 77 14.7C77.1 14.7 77.1 14.7 77.2 14.7C89.8 18.4 97.3 31.3 94.1 44.1C93.1 47.9 93.1 51.9 94.1 55.7C94.1 55.8 94.1 55.9 94.1 55.9C97.3 68.7 89.8 81.6 77.2 85.2C75.3 85.8 73.5 86.5 71.8 87.5Z"/></symbol>
<symbol id="s-gem" viewBox="0 0 100 100"><path d="M49.9 98C49.2 98 48.4 98 47.6 97.9C43.8 97.5 40.2 95.8 32.9 92.5C26.9 89.8 21 87 15.1 84.3C7 80.6 2.4 72.1 3.7 63.3C5 54.2 6.3 45.1 7.7 36C8.5 30.1 11.7 24.8 16.6 21.4C23.8 16.2 31 11 38.3 5.8C41.7 3.3 45.9 2 50.1 2C54.4 2 58.5 3.4 61.9 5.8C69.2 11.1 76.4 16.3 83.6 21.5C88.4 25 91.6 30.3 92.5 36.2C93.8 45.3 95.1 54.4 96.3 63.5C97.6 72.2 92.9 80.8 84.8 84.5C78.9 87.2 73 89.8 67 92.5C59.7 95.9 56.1 97.5 52.3 97.9C51.5 98 50.7 98 49.9 98Z"/></symbol>
</svg>
<header class="nav">
<div class="container nav__bar">
<a class="brand" href="#top"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Z" stroke="currentColor" stroke-width="1.5"/><path d="M14 2v5a1 1 0 0 0 1 1h5" stroke="currentColor" stroke-width="1.5"/><path d="M12 17v-6m0 0-2.5 2.5M12 11l2.5 2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><span>SealShare</span></a>
<button class="nav__toggle" id="navToggle" type="button" aria-expanded="false" aria-controls="navLinks" aria-label="Menu">
<svg class="icon icon-menu" aria-hidden="true"><use href="#i-menu"/></svg>
<svg class="icon icon-close" aria-hidden="true"><use href="#i-close"/></svg>
</button>
<nav aria-label="Main">
<ul class="nav__links" id="navLinks">
<li><a href="#why">Why</a></li>
<li><a href="#features">Features</a></li>
<li><a href="#screenshots">Screenshots</a></li>
<li><a href="#compare">Compare</a></li>
<li><a href="#install">Install</a></li>
<li><a href="#faq">FAQ</a></li>
<li><a class="btn btn--filled" href="https://gitea.nonameweb.ch/noNameWEB/SealShare"><svg class="icon" aria-hidden="true"><use href="#i-open-in-new"/></svg>Gitea</a></li>
</ul>
</nav>
</div>
</header>
<main id="main">
<section class="hero" aria-labelledby="hero-title">
<div class="container hero__grid">
<div class="hero__copy">
<span class="eyebrow"><svg class="icon" aria-hidden="true"><use href="#i-dns"/></svg>Self-hosted · Open source</span>
<h1 class="display" id="hero-title">Your own secure upload platform</h1>
<p class="lede">SealShare is file sharing software you install on your own server. Your company gets its own upload page to send files to customers and receive files from them &mdash; encrypted at rest, on your domain, without relying on an outside service.</p>
<div class="hero__actions">
<a class="btn btn--filled btn--lg" href="#install">Install SealShare<svg class="icon" aria-hidden="true"><use href="#i-arrow-forward"/></svg></a>
<a class="btn btn--outlined btn--lg" href="https://gitea.nonameweb.ch/noNameWEB/SealShare"><svg class="icon" aria-hidden="true"><use href="#i-open-in-new"/></svg>Source on Gitea</a>
</div>
<ul class="hero__facts">
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>No accounts for customers</li>
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Encrypted at rest</li>
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>One Docker image</li>
</ul>
</div>
<div class="hero__visual">
<svg class="hero__shape hero__shape--one" viewBox="0 0 100 100" aria-hidden="true"><use href="#s-soft-burst"/></svg>
<svg class="hero__shape hero__shape--two" viewBox="0 0 100 100" aria-hidden="true"><use href="#s-cookie-9"/></svg>
<div class="device device--laptop">
<div class="device__lid">
<div class="device__screen">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="img/screenshots/desktop/dark/01-upload-1600.webp 1600w, img/screenshots/desktop/dark/01-upload-800.webp 800w" sizes="(max-width: 960px) 94vw, 58vw">
<img src="img/screenshots/desktop/light/01-upload-800.webp" srcset="img/screenshots/desktop/light/01-upload-1600.webp 1600w, img/screenshots/desktop/light/01-upload-800.webp 800w" sizes="(max-width: 960px) 94vw, 58vw"
width="1512" height="982" loading="eager" decoding="async"
alt="SealShare&#x27;s upload page with files selected and share options">
</picture>
</div>
</div>
</div>
<div class="device device--phone">
<div class="device__screen">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="img/screenshots/phone/dark/03-download-1080.webp 1080w, img/screenshots/phone/dark/03-download-540.webp 540w" sizes="(max-width: 960px) 24vw, 15vw">
<img src="img/screenshots/phone/light/03-download-540.webp" srcset="img/screenshots/phone/light/03-download-1080.webp 1080w, img/screenshots/phone/light/03-download-540.webp 540w" sizes="(max-width: 960px) 24vw, 15vw"
width="393" height="852" loading="eager" decoding="async"
alt="The download page on a phone">
</picture>
</div>
</div>
</div>
</div>
</section>
<section class="section section--tint" id="why" aria-labelledby="why-title">
<div class="container why">
<div class="why__statement">
<span class="eyebrow"><svg class="icon" aria-hidden="true"><use href="#i-cloud-upload"/></svg>Why run your own</span>
<h2 class="headline" id="why-title">Stop routing your customers' files through someone else's service</h2>
<p class="lede">Transfer services are quick, but every file you exchange with a customer ends up on their servers, under their terms. SealShare is a simple setup for your own upload platform: a secure way to get data from customers and share data with them, entirely under your control.</p>
</div>
<div class="reasons">
<article class="reason">
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-9"/></svg><svg class="icon" aria-hidden="true"><use href="#i-dns"/></svg></span>
<h3 class="title">Your server, your data</h3>
<p>Files are stored on infrastructure you control, in the country you choose &mdash; not with a transfer service.</p>
</article>
<article class="reason">
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-clover-4"/></svg><svg class="icon" aria-hidden="true"><use href="#i-no-accounts"/></svg></span>
<h3 class="title">No accounts for customers</h3>
<p>Customers open a link and download. Nobody has to sign up anywhere.</p>
</article>
<article class="reason">
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-puffy-diamond"/></svg><svg class="icon" aria-hidden="true"><use href="#i-palette"/></svg></span>
<h3 class="title">Your name on it</h3>
<p>Your domain, and your logo, title and description on the upload page.</p>
</article>
<article class="reason">
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-sunny"/></svg><svg class="icon" aria-hidden="true"><use href="#i-money-off"/></svg></span>
<h3 class="title">No per-user pricing</h3>
<p>Open source under the MIT licence. Use it for the whole company, without seats or subscriptions.</p>
</article>
</div>
</div>
</section>
<section class="section" id="how" aria-labelledby="how-title">
<div class="container">
<div class="section__head section__head--center">
<h2 class="headline" id="how-title">How it works</h2>
<p class="lede">Three steps, for you and for the people you share with.</p>
</div>
<ol class="steps">
<li class="step">
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-9"/></svg><svg class="icon" aria-hidden="true"><use href="#i-cloud-upload"/></svg></span>
<h3 class="title">Upload</h3>
<p>Drop files or whole folders. Add a password, an expiry and a download limit if you want.</p>
</li>
<li class="step">
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-soft-burst"/></svg><svg class="icon" aria-hidden="true"><use href="#i-qr-code-2"/></svg></span>
<h3 class="title">Share</h3>
<p>Copy the link, show it as a QR code, or send it from your phone's share sheet.</p>
</li>
<li class="step">
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-gem"/></svg><svg class="icon" aria-hidden="true"><use href="#i-download"/></svg></span>
<h3 class="title">Download</h3>
<p>Your customer opens the link and downloads single files or everything as a ZIP. Expired shares delete themselves.</p>
</li>
</ol>
</div>
</section>
<section class="section section--tint" id="features" aria-labelledby="features-title">
<div class="container">
<div class="section__head">
<span class="eyebrow"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Features</span>
<h2 class="headline" id="features-title">Everything a file exchange with customers needs</h2>
<p class="lede">And nothing that turns it into a project: SealShare runs from a single container and is set up in a few minutes. It follows your system's light or dark theme, on desktop and phone.</p>
</div>
<div class="features">
<article class="feature">
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-drive-folder-upload"/></svg></span>
<h3>Files and folders</h3>
<p>Drag & drop or browse; folders keep their structure, with live upload progress.</p>
</article>
<article class="feature">
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-link"/></svg></span>
<h3>A link per share</h3>
<p>Every upload gets its own random, hard-to-guess link.</p>
</article>
<article class="feature">
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-qr-code-2"/></svg></span>
<h3>QR code and share sheet</h3>
<p>Show the link as a QR code, save it as a PNG, or pass it to the phone's share sheet.</p>
</article>
<article class="feature">
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-lock"/></svg></span>
<h3>Password protection</h3>
<p>Protect a share with a password; its encryption key is derived from it.</p>
</article>
<article class="feature">
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-encrypted"/></svg></span>
<h3>Encrypted at rest</h3>
<p>Encrypted at rest with AES-256-GCM; with a share password the key is never stored.</p>
</article>
<article class="feature">
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-schedule"/></svg></span>
<h3>Expiry</h3>
<p>From 1 hour to 30 days, or never when the admin allows it.</p>
</article>
<article class="feature">
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-counter-5"/></svg></span>
<h3>Download limits</h3>
<p>Close a share after a set number of downloads.</p>
</article>
<article class="feature">
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-folder-zip"/></svg></span>
<h3>ZIP download</h3>
<p>Recipients download everything in one archive.</p>
</article>
<article class="feature">
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-delete-sweep"/></svg></span>
<h3>Automatic clean-up</h3>
<p>Expired shares and their files are deleted every hour.</p>
</article>
<article class="feature">
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-dashboard"/></svg></span>
<h3>Admin dashboard</h3>
<p>Every share with its size, downloads and expiry; storage use at a glance.</p>
</article>
<article class="feature">
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-admin-panel-settings"/></svg></span>
<h3>Limits and quota</h3>
<p>Maximum file and share size, files per share, storage quota, default expiry.</p>
</article>
<article class="feature">
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-palette"/></svg></span>
<h3>Your branding</h3>
<p>Your logo, site title and description on the upload page.</p>
</article>
<article class="feature">
<span class="badge-icon" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-shield-lock"/></svg></span>
<h3>System password</h3>
<p>Optionally close the upload page to everyone who does not have the password.</p>
</article>
<article class="feature">
<span class="badge-icon badge-icon--secondary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-phonelink-lock"/></svg></span>
<h3>Two-factor sign-in</h3>
<p>Admins protect their accounts with authenticator app codes.</p>
</article>
<article class="feature">
<span class="badge-icon badge-icon--tertiary" aria-hidden="true"><svg viewBox="0 0 100 100"><use href="#s-cookie-6"/></svg><svg class="icon" aria-hidden="true"><use href="#i-deployed-code"/></svg></span>
<h3>One Docker image</h3>
<p>FrankenPHP with Laravel Octane, automatic TLS and SQLite &mdash; no separate database.</p>
</article>
</div>
</div>
</section>
<section class="section" id="screenshots" aria-labelledby="screenshots-title">
<div class="container">
<div class="section__head section__head--center">
<h2 class="headline" id="screenshots-title">See it in action</h2>
<p class="lede">The same interface on a laptop and a phone, in light or dark.</p>
</div>
<div class="gallery__controls">
<div class="segmented" role="group" aria-label="Device">
<button type="button" data-gallery-device="desktop" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Desktop</button>
<button type="button" data-gallery-device="phone" aria-pressed="false"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Phone</button>
</div>
<div class="segmented" role="group" aria-label="Theme">
<button type="button" data-gallery-theme="light" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Light</button>
<button type="button" data-gallery-theme="dark" aria-pressed="false"><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg>Dark</button>
</div>
</div>
<ul class="gallery gallery--desktop" data-gallery="desktop">
<li>
<figure class="shot">
<div class="device device--laptop">
<div class="device__lid">
<div class="device__screen">
<img src="img/screenshots/desktop/light/01-upload-800.webp" srcset="img/screenshots/desktop/light/01-upload-1600.webp 1600w, img/screenshots/desktop/light/01-upload-800.webp 800w" sizes="(max-width: 840px) 94vw, 568px"
data-light-src="img/screenshots/desktop/light/01-upload-800.webp" data-light-srcset="img/screenshots/desktop/light/01-upload-1600.webp 1600w, img/screenshots/desktop/light/01-upload-800.webp 800w"
data-dark-src="img/screenshots/desktop/dark/01-upload-800.webp" data-dark-srcset="img/screenshots/desktop/dark/01-upload-1600.webp 1600w, img/screenshots/desktop/dark/01-upload-800.webp 800w"
width="1512" height="982" loading="lazy" decoding="async"
alt="The upload page with two files selected and the share options filled in">
</div>
</div>
</div>
<figcaption><strong>Upload files and folders</strong><span>Drag & drop, then set a password, an expiry and a download limit.</span></figcaption>
</figure>
</li>
<li>
<figure class="shot">
<div class="device device--laptop">
<div class="device__lid">
<div class="device__screen">
<img src="img/screenshots/desktop/light/02-share-created-800.webp" srcset="img/screenshots/desktop/light/02-share-created-1600.webp 1600w, img/screenshots/desktop/light/02-share-created-800.webp 800w" sizes="(max-width: 840px) 94vw, 568px"
data-light-src="img/screenshots/desktop/light/02-share-created-800.webp" data-light-srcset="img/screenshots/desktop/light/02-share-created-1600.webp 1600w, img/screenshots/desktop/light/02-share-created-800.webp 800w"
data-dark-src="img/screenshots/desktop/dark/02-share-created-800.webp" data-dark-srcset="img/screenshots/desktop/dark/02-share-created-1600.webp 1600w, img/screenshots/desktop/dark/02-share-created-800.webp 800w"
width="1512" height="982" loading="lazy" decoding="async"
alt="The share created page with the share link, its details and the QR code button">
</div>
</div>
</div>
<figcaption><strong>Share the link</strong><span>Copy it, show it as a QR code or send it from the share sheet.</span></figcaption>
</figure>
</li>
<li>
<figure class="shot">
<div class="device device--laptop">
<div class="device__lid">
<div class="device__screen">
<img src="img/screenshots/desktop/light/03-qr-code-800.webp" srcset="img/screenshots/desktop/light/03-qr-code-1600.webp 1600w, img/screenshots/desktop/light/03-qr-code-800.webp 800w" sizes="(max-width: 840px) 94vw, 568px"
data-light-src="img/screenshots/desktop/light/03-qr-code-800.webp" data-light-srcset="img/screenshots/desktop/light/03-qr-code-1600.webp 1600w, img/screenshots/desktop/light/03-qr-code-800.webp 800w"
data-dark-src="img/screenshots/desktop/dark/03-qr-code-800.webp" data-dark-srcset="img/screenshots/desktop/dark/03-qr-code-1600.webp 1600w, img/screenshots/desktop/dark/03-qr-code-800.webp 800w"
width="1512" height="982" loading="lazy" decoding="async"
alt="The QR code dialog for a new share, with a Download button">
</div>
</div>
</div>
<figcaption><strong>A QR code for the link</strong><span>Scan it from the screen, or save it as a PNG.</span></figcaption>
</figure>
</li>
<li>
<figure class="shot">
<div class="device device--laptop">
<div class="device__lid">
<div class="device__screen">
<img src="img/screenshots/desktop/light/04-download-800.webp" srcset="img/screenshots/desktop/light/04-download-1600.webp 1600w, img/screenshots/desktop/light/04-download-800.webp 800w" sizes="(max-width: 840px) 94vw, 568px"
data-light-src="img/screenshots/desktop/light/04-download-800.webp" data-light-srcset="img/screenshots/desktop/light/04-download-1600.webp 1600w, img/screenshots/desktop/light/04-download-800.webp 800w"
data-dark-src="img/screenshots/desktop/dark/04-download-800.webp" data-dark-srcset="img/screenshots/desktop/dark/04-download-1600.webp 1600w, img/screenshots/desktop/dark/04-download-800.webp 800w"
width="1512" height="982" loading="lazy" decoding="async"
alt="The download page a recipient sees, listing the shared files">
</div>
</div>
</div>
<figcaption><strong>Your customer downloads</strong><span>Single files or everything as a ZIP, without an account.</span></figcaption>
</figure>
</li>
<li>
<figure class="shot">
<div class="device device--laptop">
<div class="device__lid">
<div class="device__screen">
<img src="img/screenshots/desktop/light/05-admin-dashboard-800.webp" srcset="img/screenshots/desktop/light/05-admin-dashboard-1600.webp 1600w, img/screenshots/desktop/light/05-admin-dashboard-800.webp 800w" sizes="(max-width: 840px) 94vw, 568px"
data-light-src="img/screenshots/desktop/light/05-admin-dashboard-800.webp" data-light-srcset="img/screenshots/desktop/light/05-admin-dashboard-1600.webp 1600w, img/screenshots/desktop/light/05-admin-dashboard-800.webp 800w"
data-dark-src="img/screenshots/desktop/dark/05-admin-dashboard-800.webp" data-dark-srcset="img/screenshots/desktop/dark/05-admin-dashboard-1600.webp 1600w, img/screenshots/desktop/dark/05-admin-dashboard-800.webp 800w"
width="1512" height="982" loading="lazy" decoding="async"
alt="The admin dashboard with share statistics, disk usage and the table of shares">
</div>
</div>
</div>
<figcaption><strong>Admin dashboard</strong><span>Every share, its size, downloads and expiry at a glance.</span></figcaption>
</figure>
</li>
<li>
<figure class="shot">
<div class="device device--laptop">
<div class="device__lid">
<div class="device__screen">
<img src="img/screenshots/desktop/light/06-admin-settings-800.webp" srcset="img/screenshots/desktop/light/06-admin-settings-1600.webp 1600w, img/screenshots/desktop/light/06-admin-settings-800.webp 800w" sizes="(max-width: 840px) 94vw, 568px"
data-light-src="img/screenshots/desktop/light/06-admin-settings-800.webp" data-light-srcset="img/screenshots/desktop/light/06-admin-settings-1600.webp 1600w, img/screenshots/desktop/light/06-admin-settings-800.webp 800w"
data-dark-src="img/screenshots/desktop/dark/06-admin-settings-800.webp" data-dark-srcset="img/screenshots/desktop/dark/06-admin-settings-1600.webp 1600w, img/screenshots/desktop/dark/06-admin-settings-800.webp 800w"
width="1512" height="982" loading="lazy" decoding="async"
alt="The admin settings page with branding and upload limits">
</div>
</div>
</div>
<figcaption><strong>Admin settings</strong><span>Upload limits, storage quota, your logo and title.</span></figcaption>
</figure>
</li>
</ul>
<ul class="gallery gallery--phone" data-gallery="phone" hidden>
<li>
<figure class="shot">
<div class="device device--phone">
<div class="device__screen">
<img src="img/screenshots/phone/light/01-upload-540.webp" srcset="img/screenshots/phone/light/01-upload-1080.webp 1080w, img/screenshots/phone/light/01-upload-540.webp 540w" sizes="(max-width: 840px) 44vw, 232px"
data-light-src="img/screenshots/phone/light/01-upload-540.webp" data-light-srcset="img/screenshots/phone/light/01-upload-1080.webp 1080w, img/screenshots/phone/light/01-upload-540.webp 540w"
data-dark-src="img/screenshots/phone/dark/01-upload-540.webp" data-dark-srcset="img/screenshots/phone/dark/01-upload-1080.webp 1080w, img/screenshots/phone/dark/01-upload-540.webp 540w"
width="393" height="852" loading="lazy" decoding="async"
alt="The upload page on a phone">
</div>
</div>
<figcaption><strong>Upload from a phone</strong><span>The same upload page, sized for one hand.</span></figcaption>
</figure>
</li>
<li>
<figure class="shot">
<div class="device device--phone">
<div class="device__screen">
<img src="img/screenshots/phone/light/02-password-540.webp" srcset="img/screenshots/phone/light/02-password-1080.webp 1080w, img/screenshots/phone/light/02-password-540.webp 540w" sizes="(max-width: 840px) 44vw, 232px"
data-light-src="img/screenshots/phone/light/02-password-540.webp" data-light-srcset="img/screenshots/phone/light/02-password-1080.webp 1080w, img/screenshots/phone/light/02-password-540.webp 540w"
data-dark-src="img/screenshots/phone/dark/02-password-540.webp" data-dark-srcset="img/screenshots/phone/dark/02-password-1080.webp 1080w, img/screenshots/phone/dark/02-password-540.webp 540w"
width="393" height="852" loading="lazy" decoding="async"
alt="The password prompt of a protected share on a phone">
</div>
</div>
<figcaption><strong>Password-protected shares</strong><span>Recipients enter the password to open a share.</span></figcaption>
</figure>
</li>
<li>
<figure class="shot">
<div class="device device--phone">
<div class="device__screen">
<img src="img/screenshots/phone/light/03-download-540.webp" srcset="img/screenshots/phone/light/03-download-1080.webp 1080w, img/screenshots/phone/light/03-download-540.webp 540w" sizes="(max-width: 840px) 44vw, 232px"
data-light-src="img/screenshots/phone/light/03-download-540.webp" data-light-srcset="img/screenshots/phone/light/03-download-1080.webp 1080w, img/screenshots/phone/light/03-download-540.webp 540w"
data-dark-src="img/screenshots/phone/dark/03-download-540.webp" data-dark-srcset="img/screenshots/phone/dark/03-download-1080.webp 1080w, img/screenshots/phone/dark/03-download-540.webp 540w"
width="393" height="852" loading="lazy" decoding="async"
alt="The list of shared files on a phone">
</div>
</div>
<figcaption><strong>Download on the go</strong><span>Files and a ZIP of all of them.</span></figcaption>
</figure>
</li>
<li>
<figure class="shot">
<div class="device device--phone">
<div class="device__screen">
<img src="img/screenshots/phone/light/04-qr-code-540.webp" srcset="img/screenshots/phone/light/04-qr-code-1080.webp 1080w, img/screenshots/phone/light/04-qr-code-540.webp 540w" sizes="(max-width: 840px) 44vw, 232px"
data-light-src="img/screenshots/phone/light/04-qr-code-540.webp" data-light-srcset="img/screenshots/phone/light/04-qr-code-1080.webp 1080w, img/screenshots/phone/light/04-qr-code-540.webp 540w"
data-dark-src="img/screenshots/phone/dark/04-qr-code-540.webp" data-dark-srcset="img/screenshots/phone/dark/04-qr-code-1080.webp 1080w, img/screenshots/phone/dark/04-qr-code-540.webp 540w"
width="393" height="852" loading="lazy" decoding="async"
alt="The full-screen QR code dialog on a phone">
</div>
</div>
<figcaption><strong>Scan to open</strong><span>The QR code fills the screen.</span></figcaption>
</figure>
</li>
</ul>
</div>
</section>
<section class="section section--tint" id="compare" aria-labelledby="compare-title">
<div class="container">
<div class="section__head">
<span class="eyebrow"><svg class="icon" aria-hidden="true"><use href="#i-dashboard"/></svg>Compare</span>
<h2 class="headline" id="compare-title">How SealShare compares</h2>
<p class="lede">Hosted transfer services keep your files on their servers; other self-hosted tools do the same job in their own way. Every value below comes from the product's own website, documentation or repository.</p>
</div>
<div class="compare">
<div class="compare__block">
<h3>Hosted transfer services</h3>
<p>Quick to use, but the files sit with the provider, under the provider's terms.</p>
<div class="table-scroll" tabindex="0" role="region" aria-label="SealShare and hosted transfer services">
<table>
<caption>SealShare and hosted transfer services · As of September 2026</caption>
<thead><tr><th scope="col"><span class="visually-hidden">Criterion</span></th><th scope="col" class="is-us">SealShare</th><th scope="col">WeTransfer</th><th scope="col">SwissTransfer</th><th scope="col">Dropbox Transfer</th><th scope="col">Google Drive links</th></tr></thead>
<tbody>
<tr><th scope="row">Files stored</th><td class="is-us">Your server</td><td>WeTransfer's cloud (EU or US)</td><td>Infomaniak, Switzerland</td><td>Dropbox's cloud (US by default)</td><td>Google's cloud</td></tr>
<tr><th scope="row">Operated by</th><td class="is-us">You</td><td>WeTransfer</td><td>Infomaniak</td><td>Dropbox</td><td>Google</td></tr>
<tr><th scope="row">Recipient needs an account</th><td class="is-us">No</td><td>No</td><td>No</td><td>No</td><td>No, with &ldquo;Anyone with the link&rdquo;</td></tr>
<tr><th scope="row">Password protection</th><td class="is-us">Yes</td><td>Yes<sup>1</sup></td><td>Yes</td><td>Standard plan and up</td><td>&mdash;</td></tr>
<tr><th scope="row">Link expiry</th><td class="is-us">Yes, 1 hour to 30 days, or never</td><td>Up to 3 or 7 days on Free<sup>1</sup></td><td>1 to 30 days<sup>4</sup></td><td>7 days; custom on Standard and up</td><td>Work or school accounts only</td></tr>
<tr><th scope="row">Download limit</th><td class="is-us">Yes</td><td>&mdash;</td><td>Yes, 1 to 250</td><td>&mdash;</td><td>&mdash;</td></tr>
<tr><th scope="row">Encryption at rest</th><td class="is-us">Yes, AES-256-GCM</td><td>Yes, AES-256</td><td>Yes, AES-256</td><td>Yes, AES-256</td><td>Yes, AES-256</td></tr>
<tr><th scope="row">End-to-end encryption</th><td class="is-us" data-compare="sealshare-e2e">No</td><td>Enterprise add-on</td><td>No</td><td>No</td><td>Higher Workspace editions</td></tr>
<tr><th scope="row">Folder upload</th><td class="is-us">Yes</td><td>No, folders are flattened</td><td>Yes</td><td>Yes</td><td>Yes</td></tr>
<tr><th scope="row">Custom branding</th><td class="is-us">Yes: logo, title, description</td><td>Ultimate plan and up</td><td>Through paid kDrive</td><td>Standard plan and up</td><td>&mdash;</td></tr>
<tr><th scope="row">Maximum file size</th><td class="is-us">Set by you (server limits)</td><td>3 GB or 100 GB a month on Free<sup>1</sup></td><td>50 GB per transfer</td><td>2 GB on Basic (free)</td><td>15 GB of free storage</td></tr>
<tr><th scope="row">Licence and cost</th><td class="is-us">MIT, free</td><td>Free (non-commercial use); Starter USD 8/month<sup>2</sup></td><td>Free</td><td>Free; Plus EUR 11.99/month<sup>2</sup></td><td>Free; 100 GB CHF 2/month<sup>2</sup></td></tr>
<tr><th scope="row">QR code for the link</th><td class="is-us">Yes</td><td>&mdash;</td><td>&mdash;</td><td>&mdash;</td><td>&mdash;</td></tr>
</tbody>
</table>
</div>
</div>
<div class="compare__block">
<h3>Self-hosted share tools</h3>
<p>Open-source apps you run yourself, like SealShare. Included are single-purpose tools that install with Docker and had a release in the last 12 months.</p>
<div class="table-scroll" tabindex="0" role="region" aria-label="SealShare and other self-hosted share tools">
<table>
<caption>SealShare and other self-hosted share tools · As of September 2026</caption>
<thead><tr><th scope="col"><span class="visually-hidden">Criterion</span></th><th scope="col" class="is-us">SealShare</th><th scope="col">Pingvin Share X</th><th scope="col">PsiTransfer</th><th scope="col">Gokapi</th><th scope="col">Erugo</th></tr></thead>
<tbody>
<tr><th scope="row">Files stored</th><td class="is-us">Your server</td><td>Your server or S3</td><td>Your server</td><td>Your server or S3</td><td>Your server</td></tr>
<tr><th scope="row">Operated by</th><td class="is-us">You</td><td>You</td><td>You</td><td>You</td><td>You</td></tr>
<tr><th scope="row">Recipient needs an account</th><td class="is-us">No</td><td>No</td><td>No</td><td>No</td><td>No</td></tr>
<tr><th scope="row">Password protection</th><td class="is-us">Yes</td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td></tr>
<tr><th scope="row">Link expiry</th><td class="is-us">Yes, 1 hour to 30 days, or never</td><td>Yes, up to a maximum you set</td><td>Yes, 1 hour to 8 weeks</td><td>Yes, after days or downloads</td><td>Yes, 7 days by default</td></tr>
<tr><th scope="row">Download limit</th><td class="is-us">Yes</td><td>View limit only</td><td>One-time download only</td><td>Yes</td><td>Yes</td></tr>
<tr><th scope="row">Encryption at rest</th><td class="is-us">Yes, AES-256-GCM</td><td>No</td><td>No</td><td>Optional</td><td>No</td></tr>
<tr><th scope="row">End-to-end encryption</th><td class="is-us" data-compare="sealshare-e2e">No</td><td>No</td><td>No</td><td>Optional</td><td>No</td></tr>
<tr><th scope="row">Folder upload</th><td class="is-us">Yes</td><td>Yes</td><td>No</td><td>No, files are listed singly<sup>3</sup></td><td>Yes, keeps structure</td></tr>
<tr><th scope="row">Custom branding</th><td class="is-us">Yes: logo, title, description</td><td>Yes: logo, name, colours, CSS</td><td>Yes, by editing templates</td><td>Yes: logo, CSS, JavaScript</td><td>Yes: logo, colours, themes</td></tr>
<tr><th scope="row">Maximum file size</th><td class="is-us">Set by you (server limits)</td><td>Set by you (1 GB per share by default)</td><td>Set by you (no limit by default)</td><td>Set by you (100 GB by default)</td><td>Set by you (2 GB per share by default)</td></tr>
<tr><th scope="row">Licence and cost</th><td class="is-us">MIT, free</td><td>BSD-2-Clause, free</td><td>BSD-2-Clause, free</td><td>AGPL-3.0, free</td><td>MIT, free</td></tr>
<tr><th scope="row">QR code for the link</th><td class="is-us">Yes</td><td>Yes</td><td>Yes</td><td>Yes</td><td>No</td></tr>
</tbody>
</table>
</div>
</div>
<p class="compare__note">SealShare encrypts files on the server as they arrive. With a share password the key is never stored; without one it is kept in SealShare's database. A value marked &ldquo;&mdash;&rdquo; could not be confirmed from the product's own sources. Checked on 13 September 2026.</p>
<div class="compare__notes">
<p class="compare__note"><sup>1</sup> WeTransfer's pricing page and its help centre disagree. The pricing page lists password protection on every plan, expiry up to 7 days and up to 100 GB a month on Free; the help centre (May 2026) lists password protection under Ultimate, expiry up to 3 days, and up to 10 transfers or 3 GB in 30 days on Free.</p>
<p class="compare__note"><sup>2</sup> The cheapest paid plan, at the price shown in Switzerland on the date above; prices differ by country. WeTransfer's Free plan is for individual, non-commercial use. Dropbox Plus has no passwords or branding for transfers; Google One 100 GB adds storage, not sharing features.</p>
<p class="compare__note"><sup>3</sup> Gokapi's web interface lists the files of a dropped folder one by one; its command-line client uploads a folder as a ZIP file.</p>
<p class="compare__note"><sup>4</sup> SwissTransfer's upload form offers 1, 3, 7, 15 or 30 days; Infomaniak's help articles describe 15 days, with an extension to 30 days offered to the sender by email.</p>
</div>
<div class="compare__block">
<h3>Sources</h3>
<ul class="compare__sources">
<li><strong>WeTransfer</strong>: <a href="https://wetransfer.com/pricing" rel="noopener">Pricing</a>, <a href="https://wetransfer.com/help-center/security-privacy/how-do-we-protect-your-files" rel="noopener">How files are protected</a>, <a href="https://wetransfer.com/help-center/subscriptions/plan-limits" rel="noopener">Plan limits</a>, <a href="https://wetransfer.com/help-center/how-to/transfer-availability" rel="noopener">Transfer availability</a>, <a href="https://wetransfer.com/help-center/how-to/download-a-transfer" rel="noopener">Downloading a transfer</a>, <a href="https://wetransfer.com/help-center/how-to/send-a-transfer" rel="noopener">Sending a transfer</a>, <a href="https://wetransfer.com/enterprise" rel="noopener">Enterprise</a></li>
<li><strong>SwissTransfer</strong>: <a href="https://www.swisstransfer.com/en" rel="noopener">SwissTransfer</a>, <a href="https://www.infomaniak.com/en/support/faq/1755/understanding-swisstransfer-data-security" rel="noopener">Data security</a>, <a href="https://www.infomaniak.com/en/support/faq/2451/getting-started-guide-swisstransfer-send-large-files-for-free" rel="noopener">Getting started</a>, <a href="https://www.infomaniak.com/en/swisstransfer-kdrive" rel="noopener">SwissTransfer with kDrive</a></li>
<li><strong>Dropbox Transfer</strong>: <a href="https://help.dropbox.com/share/dropbox-transfer" rel="noopener">Dropbox Transfer</a>, <a href="https://help.dropbox.com/share/create-dropbox-transfer" rel="noopener">Creating a transfer</a>, <a href="https://help.dropbox.com/security/physical-location-data-storage" rel="noopener">Where data is stored</a>, <a href="https://help.dropbox.com/security/how-security-works" rel="noopener">How security works</a>, <a href="https://help.dropbox.com/security/encrypted-team-folders" rel="noopener">Encrypted team folders</a>, <a href="https://www.dropbox.com/plans" rel="noopener">Plans</a></li>
<li><strong>Google Drive</strong>: <a href="https://support.google.com/drive/answer/2494822?hl=en" rel="noopener">Sharing files</a>, <a href="https://support.google.com/drive/answer/7166529" rel="noopener">Sharing folders</a>, <a href="https://support.google.com/drive/answer/10519333?hl=en" rel="noopener">Encryption in Drive</a>, <a href="https://support.google.com/a/answer/10741897?hl=en" rel="noopener">Client-side encryption</a>, <a href="https://support.google.com/a/answer/7630496?hl=en" rel="noopener">Data regions</a>, <a href="https://support.google.com/drive/answer/2424368?hl=en" rel="noopener">Uploading folders</a>, <a href="https://one.google.com/about/plans?hl=en" rel="noopener">Google One plans</a></li>
<li><strong>Pingvin Share X</strong>: <a href="https://github.com/smp46/pingvin-share-x/blob/main/README.md" rel="noopener">README</a>, <a href="https://github.com/smp46/pingvin-share-x/blob/main/config.example.yaml" rel="noopener">Example configuration</a>, <a href="https://github.com/smp46/pingvin-share-x/blob/main/CHANGELOG.md" rel="noopener">Changelog</a>, <a href="https://smp46.github.io/pingvin-share-x/" rel="noopener">Documentation</a>, <a href="https://github.com/smp46/pingvin-share-x" rel="noopener">Repository</a></li>
<li><strong>PsiTransfer</strong>: <a href="https://github.com/psi-4ward/psitransfer/blob/master/README.md" rel="noopener">README</a>, <a href="https://github.com/psi-4ward/psitransfer/blob/master/config.js" rel="noopener">Configuration</a>, <a href="https://github.com/psi-4ward/psitransfer/blob/master/docs/layout-customization.md" rel="noopener">Layout customisation</a>, <a href="https://github.com/psi-4ward/psitransfer" rel="noopener">Repository</a></li>
<li><strong>Gokapi</strong>: <a href="https://gokapi.readthedocs.io/en/latest/index.html" rel="noopener">Documentation</a>, <a href="https://gokapi.readthedocs.io/en/latest/usage.html" rel="noopener">Usage</a>, <a href="https://gokapi.readthedocs.io/en/latest/setup.html" rel="noopener">Setup</a>, <a href="https://gokapi.readthedocs.io/en/latest/advanced.html" rel="noopener">Advanced</a>, <a href="https://github.com/Forceu/Gokapi/blob/master/LICENSE.md" rel="noopener">Licence</a></li>
<li><strong>Erugo</strong>: <a href="https://github.com/ErugoOSS/Erugo/blob/main/README.md" rel="noopener">README</a>, <a href="https://erugo.app/docs/reference/configuration" rel="noopener">Configuration</a>, <a href="https://erugo.app/docs/guides/branding" rel="noopener">Branding</a>, <a href="https://github.com/ErugoOSS/Erugo" rel="noopener">Repository</a></li>
</ul>
</div>
</div>
</div>
</section>
<section class="section" id="install" aria-labelledby="install-title">
<div class="container install">
<div class="install__copy">
<span class="eyebrow"><svg class="icon" aria-hidden="true"><use href="#i-deployed-code"/></svg>Install</span>
<h2 class="headline" id="install-title">Running in a few minutes</h2>
<p class="lede">All you need is a server with Docker and a domain name.</p>
<ul class="install__list">
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg><span>The image includes the web server with automatic TLS certificates, and uses SQLite &mdash; no separate database.</span></li>
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg><span>Migrations run on start. Open your domain and the setup wizard creates the first admin account.</span></li>
<li><svg class="icon" aria-hidden="true"><use href="#i-check"/></svg><span>Your files and database live in Docker volumes on your server.</span></li>
</ul>
<a class="btn btn--tonal" href="https://gitea.nonameweb.ch/noNameWEB/SealShare"><svg class="icon" aria-hidden="true"><use href="#i-open-in-new"/></svg>Full instructions on Gitea</a>
</div>
<div class="code">
<div class="code__bar"><svg class="icon" aria-hidden="true"><use href="#i-terminal"/></svg><span>Production, with Docker Compose</span></div>
<pre><code>mkdir sealshare &amp;&amp; cd sealshare
curl -O https://raw.githubusercontent.com/surtic86/SealShare/main/docker-compose.example.yml
cp docker-compose.example.yml docker-compose.yml
<span class="comment"># Generate an app key and paste it into docker-compose.yml</span>
docker run --rm ghcr.io/surtic86/sealshare:latest php artisan key:generate --show
<span class="comment"># Edit docker-compose.yml — set APP_KEY, APP_URL, and SERVER_NAME</span>
<span class="comment"># Then start:</span>
docker compose up -d</code></pre>
</div>
</div>
</section>
<section class="section section--tint" id="faq" aria-labelledby="faq-title">
<div class="container">
<div class="section__head section__head--center">
<h2 class="headline" id="faq-title">Frequently asked questions</h2>
</div>
<div class="faq">
<details>
<summary>Is SealShare end-to-end encrypted?<svg class="icon" aria-hidden="true"><use href="#i-expand-more"/></svg></summary>
<div class="faq__answer">
<p>No. SealShare encrypts files on your server as they arrive, with AES-256-GCM in chunks, and stores them only in encrypted form. Because the server does the encrypting, it handles the files in the clear while they are uploaded and downloaded &mdash; which is why it matters that the server is yours.</p>
<p>For a share without a password the key is kept in SealShare's database. With a share password the key is derived from that password and never stored, so the files cannot be decrypted without it.</p>
</div>
</details>
<details>
<summary>Can customers send files to us?<svg class="icon" aria-hidden="true"><use href="#i-expand-more"/></svg></summary>
<div class="faq__answer">
<p>Yes. Anyone who can open your upload page can upload and gets a link to send back to you. To keep the page for the people you choose, turn on the optional system password in the admin settings and give it to your customers.</p>
</div>
</details>
<details>
<summary>How big can files be?<svg class="icon" aria-hidden="true"><use href="#i-expand-more"/></svg></summary>
<div class="faq__answer">
<p>As big as your server allows. The Docker image accepts up to 4 GB per file by default; within that, you set the maximum file size and share size in the admin settings (100 MB and 2 GB out of the box). Behind a reverse proxy, raise its request size and timeouts as well.</p>
</div>
</details>
<details>
<summary>What does it cost?<svg class="icon" aria-hidden="true"><use href="#i-expand-more"/></svg></summary>
<div class="faq__answer">
<p>Nothing. SealShare is open source under the MIT licence. You pay only for the server you run it on.</p>
</div>
</details>
<details>
<summary>Who runs it?<svg class="icon" aria-hidden="true"><use href="#i-expand-more"/></svg></summary>
<div class="faq__answer">
<p>You do. There is no SealShare cloud and no account with us: you install the Docker image on a server you run, and your files stay on it instead of passing through a transfer service.</p>
</div>
</details>
</div>
</div>
</section>
<section class="section" id="contact" aria-labelledby="contact-title">
<div class="container">
<div class="contact">
<div>
<h2 class="headline" id="contact-title">Questions or feedback?</h2>
<p>Get in touch, or open an issue on Gitea.</p>
<a href="mailto:surtic86@gmail.com"><svg class="icon" aria-hidden="true"><use href="#i-mail"/></svg>surtic86@gmail.com</a>
</div>
<a class="btn btn--filled btn--lg" href="https://gitea.nonameweb.ch/noNameWEB/SealShare/issues"><svg class="icon" aria-hidden="true"><use href="#i-bug-report"/></svg>Open an issue</a>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="container">
<div class="footer__grid">
<div>
<a class="brand" href="#top"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Z" stroke="currentColor" stroke-width="1.5"/><path d="M14 2v5a1 1 0 0 0 1 1h5" stroke="currentColor" stroke-width="1.5"/><path d="M12 17v-6m0 0-2.5 2.5M12 11l2.5 2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><span>SealShare</span></a>
<p class="footer__blurb">Self-hosted file sharing: your company's own upload platform to exchange files with customers.</p>
</div>
<div>
<h2>On this site</h2>
<ul>
<li><a href="#features">Features</a></li>
<li><a href="#compare">Compare</a></li>
<li><a href="#install">Install</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
</div>
<div>
<h2>Project</h2>
<ul>
<li><a href="https://gitea.nonameweb.ch/noNameWEB/SealShare">Source on Gitea</a></li>
<li><a href="https://gitea.nonameweb.ch/noNameWEB/SealShare/src/branch/main/CHANGELOG.md">Changelog</a></li>
<li><a href="https://gitea.nonameweb.ch/noNameWEB/SealShare/src/branch/main/LICENSE">MIT licence</a></li>
<li><a href="privacy.html">Privacy</a></li>
</ul>
</div>
</div>
<div class="footer__bottom">
<span>&copy; 2026 <a href="https://nonameweb.ch">noNameWEB</a></span>
<span>SealShare is open-source software under the MIT licence.</span>
</div>
</div>
</footer>
<script>
// Phone menu.
const navToggle = document.getElementById('navToggle');
const navLinks = document.getElementById('navLinks');
const closeNav = () => {
navLinks.classList.remove('is-open');
navToggle.setAttribute('aria-expanded', 'false');
};
navToggle.addEventListener('click', () => {
const open = navLinks.classList.toggle('is-open');
navToggle.setAttribute('aria-expanded', String(open));
});
navLinks.querySelectorAll('a').forEach((link) => link.addEventListener('click', closeNav));
document.addEventListener('click', (event) => {
if (!event.target.closest('.nav')) {
closeNav();
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && navLinks.classList.contains('is-open')) {
closeNav();
navToggle.focus();
}
});
// Screenshot gallery: one device at a time, in the chosen theme (the visitor's, to begin with).
const deviceButtons = document.querySelectorAll('[data-gallery-device]');
const themeButtons = document.querySelectorAll('[data-gallery-theme]');
const galleries = document.querySelectorAll('[data-gallery]');
const shots = document.querySelectorAll('[data-gallery] img');
const press = (buttons, pressed) => buttons.forEach((button) => button.setAttribute('aria-pressed', String(button === pressed)));
const showDevice = (device) => {
press(deviceButtons, [...deviceButtons].find((button) => button.dataset.galleryDevice === device));
galleries.forEach((gallery) => { gallery.hidden = gallery.dataset.gallery !== device; });
};
const showTheme = (theme) => {
press(themeButtons, [...themeButtons].find((button) => button.dataset.galleryTheme === theme));
shots.forEach((shot) => {
shot.srcset = shot.dataset[theme + 'Srcset'];
shot.src = shot.dataset[theme + 'Src'];
});
};
deviceButtons.forEach((button) => button.addEventListener('click', () => showDevice(button.dataset.galleryDevice)));
themeButtons.forEach((button) => button.addEventListener('click', () => showTheme(button.dataset.galleryTheme)));
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
showTheme('dark');
}
</script>
</body>
</html>
+126
View File
@@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Privacy — SealShare</title>
<meta name="description" content="How the SealShare website handles visitor data: server logs at the host and Plausible analytics, nothing else.">
<meta name="theme-color" content="#faf4ff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#10062d" media="(prefers-color-scheme: dark)">
<script defer data-domain="sealshare.nonameweb.ch" src="https://plausible.io/js/script.js"></script>
<link rel="icon" href="img/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="img/icon.png">
<link rel="preload" href="fonts/GoogleSansFlex-Latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="css/theme.css">
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<svg xmlns="http://www.w3.org/2000/svg" class="sprite" aria-hidden="true">
<symbol id="i-menu" viewBox="0 -960 960 960"><path d="M160-240q-17 0-28.5-11.5T120-280q0-17 11.5-28.5T160-320h640q17 0 28.5 11.5T840-280q0 17-11.5 28.5T800-240H160Zm0-200q-17 0-28.5-11.5T120-480q0-17 11.5-28.5T160-520h640q17 0 28.5 11.5T840-480q0 17-11.5 28.5T800-440H160Zm0-200q-17 0-28.5-11.5T120-680q0-17 11.5-28.5T160-720h640q17 0 28.5 11.5T840-680q0 17-11.5 28.5T800-640H160Z"/></symbol>
<symbol id="i-close" viewBox="0 -960 960 960"><path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/></symbol>
<symbol id="i-open-in-new" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h240q17 0 28.5 11.5T480-800q0 17-11.5 28.5T440-760H200v560h560v-240q0-17 11.5-28.5T800-480q17 0 28.5 11.5T840-440v240q0 33-23.5 56.5T760-120H200Zm560-584L416-360q-11 11-28 11t-28-11q-11-11-11-28t11-28l344-344H600q-17 0-28.5-11.5T560-800q0-17 11.5-28.5T600-840h200q17 0 28.5 11.5T840-800v200q0 17-11.5 28.5T800-560q-17 0-28.5-11.5T760-600v-104Z"/></symbol>
</svg>
<header class="nav">
<div class="container nav__bar">
<a class="brand" href="index.html"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Z" stroke="currentColor" stroke-width="1.5"/><path d="M14 2v5a1 1 0 0 0 1 1h5" stroke="currentColor" stroke-width="1.5"/><path d="M12 17v-6m0 0-2.5 2.5M12 11l2.5 2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><span>SealShare</span></a>
<button class="nav__toggle" id="navToggle" type="button" aria-expanded="false" aria-controls="navLinks" aria-label="Menu">
<svg class="icon icon-menu" aria-hidden="true"><use href="#i-menu"/></svg>
<svg class="icon icon-close" aria-hidden="true"><use href="#i-close"/></svg>
</button>
<nav aria-label="Main">
<ul class="nav__links" id="navLinks">
<li><a href="index.html#why">Why</a></li>
<li><a href="index.html#features">Features</a></li>
<li><a href="index.html#screenshots">Screenshots</a></li>
<li><a href="index.html#compare">Compare</a></li>
<li><a href="index.html#install">Install</a></li>
<li><a href="index.html#faq">FAQ</a></li>
<li><a class="btn btn--filled" href="https://gitea.nonameweb.ch/noNameWEB/SealShare"><svg class="icon" aria-hidden="true"><use href="#i-open-in-new"/></svg>Gitea</a></li>
</ul>
</nav>
</div>
</header>
<main id="main" class="legal">
<div class="container">
<article>
<h1 class="headline">Privacy</h1>
<p class="meta">Last updated: September 2026</p>
<p>This page explains what happens to your data when you visit sealshare.nonameweb.ch. It is about this website only. SealShare itself is software that companies install and run on their own servers; this website does not host it, and files shared through a SealShare installation never reach us.</p>
<h2>Who runs this website</h2>
<p>This website is run by noNameWEB, Switzerland. For any question about your data, write to <a href="mailto:surtic86@gmail.com">surtic86@gmail.com</a>.</p>
<h2>Hosting and server logs</h2>
<p>The website is hosted by METANET AG in Switzerland. When you open a page, the web server records technical data in its logs, as every web server does: your IP address, the date and time, the page requested, your browser's user agent and the referring page. These logs keep the site running and secure; they are not used to identify you and are kept only as long as the host's standard retention.</p>
<h2>Analytics</h2>
<p>We count visits with <a href="https://plausible.io">Plausible Analytics</a>, a privacy-friendly service hosted in the EU. Plausible uses no cookies and stores no personal data: it does not keep your IP address or build a profile of you. What we see are totals, such as page views, referring sites, countries and device types. See <a href="https://plausible.io/data-policy">Plausible's data policy</a>.</p>
<h2>No other third parties</h2>
<ul>
<li>The fonts, images and styles are served from this website. Nothing is loaded from Google or other font and content networks.</li>
<li>There are no advertising, social media or tracking scripts besides Plausible.</li>
<li>The website sets no cookies and has no forms.</li>
</ul>
<h2>Links to other sites</h2>
<p>Links to Gitea, the source repository and Plausible lead to sites with their own privacy policies.</p>
<h2>Your rights</h2>
<p>Under the Swiss Federal Act on Data Protection you can ask what data about you is processed, and have it corrected or deleted. Write to the address above.</p>
<h2>Changes</h2>
<p>If this website starts handling data differently, this page is updated and its date changed.</p>
</article>
</div>
</main>
<footer class="footer">
<div class="container">
<div class="footer__grid">
<div>
<a class="brand" href="index.html"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Z" stroke="currentColor" stroke-width="1.5"/><path d="M14 2v5a1 1 0 0 0 1 1h5" stroke="currentColor" stroke-width="1.5"/><path d="M12 17v-6m0 0-2.5 2.5M12 11l2.5 2.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg><span>SealShare</span></a>
<p class="footer__blurb">Self-hosted file sharing: your company's own upload platform to exchange files with customers.</p>
</div>
<div>
<h2>On this site</h2>
<ul>
<li><a href="index.html#features">Features</a></li>
<li><a href="index.html#compare">Compare</a></li>
<li><a href="index.html#install">Install</a></li>
<li><a href="index.html#faq">FAQ</a></li>
</ul>
</div>
<div>
<h2>Project</h2>
<ul>
<li><a href="https://gitea.nonameweb.ch/noNameWEB/SealShare">Source on Gitea</a></li>
<li><a href="https://gitea.nonameweb.ch/noNameWEB/SealShare/src/branch/main/CHANGELOG.md">Changelog</a></li>
<li><a href="https://gitea.nonameweb.ch/noNameWEB/SealShare/src/branch/main/LICENSE">MIT licence</a></li>
<li><a href="privacy.html">Privacy</a></li>
</ul>
</div>
</div>
<div class="footer__bottom">
<span>&copy; 2026 <a href="https://nonameweb.ch">noNameWEB</a></span>
<span>SealShare is open-source software under the MIT licence.</span>
</div>
</div>
</footer>
<script>
const navToggle = document.getElementById('navToggle');
const navLinks = document.getElementById('navLinks');
navToggle.addEventListener('click', () => {
const open = navLinks.classList.toggle('is-open');
navToggle.setAttribute('aria-expanded', String(open));
});
</script>
</body>
</html>