Skip to content
Skip to content
Get started
/Developers

Developers

Architecture

zcrypt is a thin, stateless server wrapped around a client-side encryption pipeline and your own storage accounts. Here is how the pieces connect, and why the server can never read your files.

Overview

A file's life in zcrypt is: your browser splits it into chunks and compresses and encrypts each one; the chunks travel over a chunked HTTP API to the Go backend; the backend stages them on durable disk and a background worker syncs them to a storage platform you own; a PostgreSQL index records the ciphertext metadata that lets you find and reassemble the file later. Plaintext and keys never leave your device.

The big picture

data flow
Your device (browser / app)            zcrypt backend (Go, stateless)         Your storage
┌─────────────────────────┐            ┌──────────────────────────┐         ┌──────────────┐
│  1. zstd compress        │            │  Chunked HTTP API        │         │  GitHub      │
│  2. AES-256-GCM encrypt  │  chunks    │  (JSON + raw chunk bytes)│         │  GitLab      │
│  3. split into chunks    │ ─────────► │            │             │ ──────► │  Hugging Face│
│                          │   HTTPS    │            ▼             │  sync   │  Telegram    │
│  passphrase ─► key       │            │  Durable disk staging    │         └──────────────┘
│  (never leaves device)   │ ◄───────── │            │             │
└─────────────────────────┘  SSE/events│            ▼             │
                                        │  Background sync worker  │
                                        │            │             │
                                        │            ▼             │
                                        │  PostgreSQL index (pgx)  │
                                        └──────────────────────────┘
                                  Server sees only ciphertext + metadata

Client pipeline

Everything that makes a file private happens before it leaves your device, in this order:

  1. Chunk. The file is split into chunks sized to your device (4, 8, 10 or 16 MiB; 10 MiB is the usual). Chunking is what makes large files fast, parallel, and resumable. Each chunk is then processed on its own.
  2. Compress and encrypt. Each chunk is compressed with zstd (skipped when it would not shrink), then sealed with AES-256-GCM under a random per-file key. That key is wrapped by a key derived from your passphrase (PBKDF2-SHA256, 600,000 iterations, with a unique per-file salt). Authenticated encryption means tampering is detectable on the way back.
  3. Hash. Each encrypted chunk is hashed with SHA-256, so the server and your storage can be checked against it without anything being decrypted.

In the web app this pipeline runs in a pool of Web Workers, so compression, encryption, and hashing happen off the main thread and in parallel across CPU cores. Chunk buffers are handed to and from the workers by transfer (zero-copy, no duplicate allocation), and the pool size scales to the device’s cores and memory. The same worker pool runs the reverse pipeline on download.

The same pipeline runs everywhere zcrypt runs: in the web app’s Web Worker pool, in zcrypt-core, the native Rust engine behind the desktop and Android apps, so a file uploaded from any one of them decrypts cleanly in the others.

Download & streaming to disk

Downloads run the client pipeline in reverse. The app fetches each encrypted chunk, verifies its SHA-256, then decrypts and (where needed) decompresses it in the same Web Worker pool, so a large download parallelizes across cores instead of freezing the tab.

Small files are reassembled in memory and saved as a download. A file too large to hold in a browser tab is instead streamed straight to disk: using the browser’s File System Access API, the app opens a writable to the file you pick, writes chunks in order as they are decrypted, and hashes them in write-order, so peak memory stays flat no matter how large the file. The whole-file hash is checked before the file is committed; an integrity mismatch or a cancel discards the partial file rather than leaving a corrupt one behind.

Downloads are resumable within a session: a pause or a transient network failure keeps what has already been decrypted (or written to disk), so a later run continues from the high-water mark instead of restarting at chunk zero.

Chunked HTTP API

The backend is plain Go standard-library net/http, no web framework. The client opens an upload session, then sends each encrypted chunk as raw bytes with a SHA-256 header the server uses to verify integrity (never to decrypt). Where a platform supports it, the client can request a presigned URL and upload a chunk straight to the platform, skipping the server relay. Downloads run the same path in reverse: metadata first, then chunks by index.

Because uploads are session-based and chunk-addressed, they are inherently resumable, the client asks which chunks already landed and re-sends only the gaps.

Staging & the sync worker

Uploaded chunks are not pushed to a storage platform inline. The server first writes each chunk to a durable staging directory on disk, which survives restarts. A background sync worker then drains that staging area, pushing chunks to the right platform repository and recording the result. This decouples the user-facing upload from the slower, retry-prone platform push.

  • •Resilience. If a platform push fails, the chunk stays staged and is retried; a crash mid-upload doesn't lose already-received chunks.
  • •Read-through. If you download a file whose chunks haven't been synced off staging yet, the server serves them straight from the staging directory.
  • •Progress. Upload and sync progress is pushed to the client over Server-Sent Events.

Desktop and Android skip the relay

This staging-and-relay path is how the web app moves chunks. The desktop and Android apps upload directly to your connected platform using credentials from your OS keychain, the backend only receives metadata (file ID, chunk index, platform, ciphertext hash), never the chunk bytes or your platform token. See Desktop app.

Platform adapters & repo pool

Each storage backend implements a common adapter interface, upload, download, delete, create repo, list chunks, and report repo size, so the pipeline treats GitHub, GitLab, Hugging Face, and Telegram uniformly. Optional capabilities layer on top: some adapters batch many chunk uploads into a single commit, and some support direct presigned uploads.

A repo pool sits in front of the adapters. Each file's chunks go to a single platform, and as a repository approaches that platform's size threshold the pool auto-rotates to a fresh repo. Once you connect your own account, your usable space is bounded by that account, not by a fixed cap. Until then, uploads use zcrypt's shared storage, which is capped at 1 GiB per account.

Disguise

Repositories, commit messages, and stored chunk file names are generated to look like ordinary developer activity, so the storage side reveals nothing about your real files. See Storage obfuscation.

The index (PostgreSQL)

A PostgreSQL database, accessed through pgx with raw SQL (no ORM), is the index that ties everything together: users, folders and files, per-file salts and chunk references, encrypted platform tokens, shares, and more. Every record about a file is metadata or ciphertext: the database holds no readable file contents and no passphrase. The schema is applied automatically on startup.

Shared spaces & key grants

Sharing keeps the zero-knowledge boundary intact by moving keys, never plaintext. Every user has an X25519 keypair generated on their device; the private key is wrapped under their passphrase-derived key, so the server stores only ciphertext, while the public key and a short fingerprint are public so others can seal grants to them.

A shared space (“shared vault”) has its own random symmetric key. To grant a member access, that space key is sealed to the member’s public key with an ECIES construction, an ephemeral X25519 key does ECDH with the recipient, and the shared secret is hashed into an AES-256-GCM wrapping key, and each shared file’s content key is re-wrapped under the space key. The server holds only these opaque grants: it can store, hand out, and delete them, but can never open one. Removing a member and rotating the space key re-seals a fresh key to the people who remain and re-wraps every file under it, so the old grant is dead.

Public folder links work the same way: the link key is generated on the client and lives only in the URL fragment, and each file’s content key is re-wrapped under it, the server serving the link never sees the key.

The zero-knowledge boundary

The line between your device and the server is the security boundary. Cross it, and only ciphertext and metadata exist:

  • •Your passphrase, and the key derived from it, never leave the client. They are never sent, stored, or logged.
  • •Chunks arrive already encrypted; the server verifies their hashes but cannot decrypt them.
  • •Platform tokens are the one secret the server does hold, and they're encrypted at rest with a per-user key derived from the instance MASTER_KEY via HKDF, so the storage platform never sees plaintext, and the database never sees a usable token.

The trade-off

Because the server has nothing to decrypt your files with, neither do we, and neither does anyone who compromises the server. The flip side is that a forgotten passphrase is unrecoverable. See Zero-knowledge architecture.

Where to go next