Transfers
Uploading
Files are compressed, encrypted, and chunked on your device, then streamed to your storage one chunk at a time. The upload survives refreshes, drops, and even a switch to another device, and it lands exactly where you are in your drive.
Chunked & client-encrypted
By the time an upload begins, the file is split into chunks on your device, and each chunk has been compressed with zstd and encrypted with AES-256-GCM (see How it works). What gets sent is a sequence of encrypted chunks: the server receives only ciphertext.
Chunks are sized to your device: 4, 8, 10 or 16 MiB, from a memory-constrained phone up to a powerful desktop, with 10 MiB the common middle. The chosen size is recorded server-side, so a resume from any device re-slices the file at exactly the same boundaries: a mismatched size would misalign every chunk after the first.
An upload starts by opening a session that records the file’s metadata, salt, wrapped key, chunk count, and chunk size. Each chunk is then sent with its own SHA-256 hash, which the server checks on arrival; a chunk whose bytes don’t match its hash is rejected.
Resumable by design
Uploads are resumable, and the design makes resuming reliable rather than best-effort:
- •Incoming chunks are staged durably on the server and recorded in the database before the platform push, so they survive a server restart.
- •A status endpoint reports which chunk indices have already been received: including chunks still staged and merely waiting on the background push. Resume counts those too, so it never re-sends bytes the server already holds.
- •On resume, the client asks for that status and continues from the chunks still missing, re-using the same session and the same per-file key: already-received chunks are skipped, not re-sent.
- •A chunk that does arrive twice is rejected early and cheaply: the server recognizes it by index before it reads the body, so a duplicate multi-MB chunk is turned away without being uploaded again, and never stored twice.
Two stages, both tracked
A chunk is first staged on the server, then a background worker pushes it to your storage platform. Completion is reported once every chunk has been received; durability on the platform follows immediately after.Reloads & device switches
A partial upload survives a page reload. As soon as a session exists, the client saves a small pointer to it (session id, file identity, chunk layout), never the key. Re-add the same file after a refresh and the client re-derives the file’s key from the server-stored wrapped key plus your passphrase, then picks up from the chunks already received. The raw key is never written to disk, so this stays zero-knowledge.
Even with no local pointer at all: a different device, a cleared browser, a fresh session, resume still works, because the server is the source of truth. When you start an upload for a file that already has an active session (matched by its content hash and size), the server hands that session back instead of creating a duplicate, pinned to its original storage platform. The client adopts it and continues.
You’ll be asked to re-pick the file
A browser can’t re-read a file’s bytes on its own after a reload, so resuming means re-selecting the same file. The client checks the name and size match the pending session before continuing: picking the wrong file would corrupt the result, so a mismatch is refused.Unfinished uploads
Interrupted uploads don’t just vanish. An unfinished uploads panel lists every session you started but never finished: showing the filename, size, the storage platform it was headed to, how far it got, and when it expires. Each one gives you two choices:
- •Resume. Re-select the file and the upload continues from the server’s already-received chunks, on its original platform.
- •Discard. Cancel the session and delete its staged chunks server-side: the deliberate “give up” path.
Unfinished uploads are kept for 7 days, then removed automatically, long enough that an interrupted multi-GB upload can wait for you to get back to a good connection.
Pause, cancel & honest progress
Pausing is not cancelling. A pause stops the transfer at three layers at once: the loop stops launching new chunks, chunks that already finished encrypting check the paused state before touching the network, and any transfer already on the wire is aborted, while leaving the server session and the resume context intact. Resume simply continues from where it stopped. Cancel is the opposite: it tears down the session and deletes staged data, so it can’t be resumed.
Dismissing a row keeps it recoverable
Clearing an upload from the transfer dock is non-destructive: it stops the local run but leaves the server session alive, so the upload reappears under unfinished uploads. Only an explicit Cancel/Discard throws the staged data away.The percentage you see is honest. Progress is measured in real bytes sent, completed chunks count their full size, and a chunk still in flight contributes its sent bytes at a slight discount, since reaching the server isn’t the same as being confirmed. The displayed number is monotonic: it never runs backwards, so a retried or aborted chunk can’t make the bar dip. Hashing a large file before upload shows in the stage label rather than eating into the bar, and a resume starts the bar at its true position instead of snapping to zero.
Relay vs. direct upload
zcrypt supports two upload paths, chosen automatically per platform:
- •Relay (default). Chunks go to the zcrypt server, which stages them and forwards them to the platform. This works for every backend and keeps the client’s job simple.
- •Direct / presigned. For Hugging Face, which uses Git LFS, the server hands back a presigned URL and the client uploads the chunk straight to LFS storage, bypassing the server relay. This removes the double hop and is well suited to large files. The directly-uploaded chunks are then confirmed and committed in a single batch.
Still ciphertext either way
The chunk is already encrypted before it leaves your device, so neither path, nor the presigned storage URL, ever handles readable data.Parallel, and mobile-friendly
Multiple files and multiple chunks upload in parallel for speed, but concurrency is capped on both ends. The client limits how many transfers run at once based on your device, and the server bounds concurrent chunk processing: including a small per-repository limit, since some platforms create one commit per file and concurrent commits to the same repo would otherwise collide. The result is fast uploads without overwhelming memory or the platform’s API.
Dropping a batch of files from a phone works the same way. A batch of many small files uploads several at a time rather than strictly one-by-one, and the most expensive setup steps (deriving your key and spinning up the encryption workers) are done once per batch and shared across every file, so a folder of photos doesn’t pay that cost dozens of times over.
Folder-aware uploads
An upload lands in the folder you are currently viewing: the file is created directly in that folder rather than dropped at the root and moved afterward.
If the destination is a password-protected folder, the upload is wrapped under that folder’s password instead of your vault passphrase, so the file is sealed to the folder from the moment it is stored. (This is also why resuming a protected-folder upload requires that folder to be unlocked, the remaining chunks must be encrypted with the same key as the ones already sent.)
As for which storage a file lands on: when the platform picker is on Auto, the server resolves the destination for you (Telegram first), and a resume always stays on the session’s original platform. See Platform adapters for how that routing works.