File transfer is one of those categories that looks solved until you ask harder questions. The hard part is not letting users click a button and upload a file. The hard part is preserving a usable experience while refusing to treat the server as a trusted holder of plaintext, especially when the receiver may be either a human in a browser or an agent running on a remote machine, inside a container, or in an automated workflow.

Xdrop is positioned very clearly. It is not a generic cloud drive replacement, and it is not only a browser-based encrypted share page. Its scope is much sharper: an open source end-to-end encrypted file transfer system for humans and agents, with one core promise that is both restrained and meaningful: plaintext file names, plaintext contents, and decryption keys stay off the server.

This article analyzes its main design choices and engineering tradeoffs.

1. The real problem Xdrop solves: trustworthy handoffs, not generic uploads

Most file-sharing systems implicitly assume that the server both receives the file and understands the file. It knows the filename, the directory structure, and often the content as well. The differences usually come down to whether the product has accounts, expiration, or share links.

Xdrop takes a different route. It splits the system into three boundaries.

1.1 The sender understands plaintext; the server coordinates lifecycle

In Xdrop, the sender browser or local agent runtime is the only place that fully understands the file. Names, paths, MIME types, timestamps, and chunk metadata enter a browser-side encryption flow first, then move to the backend as ciphertext or opaque identifiers.

The backend is intentionally reduced to a narrower set of jobs:

  • create transfer records and return a manage token plus upload limits
  • store file and chunk progress metadata
  • issue time-limited upload and download URLs for object storage
  • enforce expiry, deletion, rate limits, and baseline access controls

That makes it a coordination plane, not a plaintext data plane.

1.2 The audience is not only human browser users, but also agents

Xdrop consistently defines itself as being “for humans and agents,” and that is not just positioning language. It is a real protocol constraint. Browser sharing and agent-driven terminal uploads are not parallel systems. They use the same transfer format, the same share-link structure, and the same decryption model.

That matters because many products split “web upload” and “automation handoff” into separate feature lines, which tends to create awkward outcomes:

  • the browser flow is polished, but automation falls back to plaintext upload
  • the CLI works, but its links are incompatible with the browser flow
  • the crypto story differs by entry point, raising audit and maintenance cost

Xdrop instead uses one protocol. The same /t/:transferId#k=... link can be opened by a human in a browser or used by an agent for local decryption.

1.3 This is not “zero metadata,” but “the server does not hold the important plaintext”

This nuance is important. Xdrop does not pretend the server sees nothing. In this architecture, the server still sees operational metadata, such as:

  • transfer creation and expiry timestamps
  • file counts, chunk counts, and ciphertext sizes
  • rate-limit identifiers
  • which chunks have completed upload

What it deliberately keeps off the server are the most semantically revealing parts of the transfer: filenames, paths, file contents, and decryption keys. That is a more accurate claim, and a more credible one.

The cryptographic design in Xdrop is not flashy. It is disciplined. Instead of inventing anything novel, it composes a few mature primitives through the browser’s built-in WebCrypto APIs.

When a sender starts a transfer, the client generates two 32-byte random secrets:

  • rootKey: the transfer root key that actually protects the manifest and file payloads
  • linkKey: the share-link key that wraps the rootKey and is placed in #k=... inside the final link

The final link looks like this in principle:

/t/<transferId>#k=<base64url-link-key>

The server stores a wrappedRootKey, not the rootKey itself. When the receiver opens the link, the browser extracts the linkKey from the fragment, unwraps the rootKey locally, and only then decrypts the manifest and file data.

This structure yields two direct benefits:

  • the transfer ID can remain in the URL path where the server can route on it
  • the actual decryption capability stays in the fragment and does not travel as part of normal HTTP requests

The key point is simple: the link fragment stays in the browser and does not travel with normal HTTP requests.

2.2 HKDF scopes keys by purpose, and AES-256-GCM provides confidentiality and integrity

Xdrop uses HKDF-SHA-256 to derive purpose-scoped AES-256-GCM keys:

  • linkKey with info = "wrap-root" derives the key that wraps the root key
  • rootKey with info = "manifest" derives the manifest encryption key
  • rootKey with info = "file:<fileId>" derives a separate key for each file

This “same source secret, different derived contexts” approach is safer than reusing a single AES key across everything. It isolates cryptographic roles and leaves room for future evolution without entangling unrelated formats.

2.3 Chunk encryption binds context, not just bytes

Xdrop uses AES-GCM for file chunks, but it does more than encrypt slices of data. In encryptChunk(), it binds the following values into authenticated data:

  • transferId
  • fileId
  • chunkIndex
  • plaintextChunkSize
  • protocol version

That means a ciphertext chunk cannot be replayed across transfers, files, or positions without failing authentication.

The IV strategy is also worth noting. Each 12-byte GCM IV is built from:

  • an 8-byte per-file noncePrefix
  • a 4-byte chunkIndex

This combines file-level randomness with deterministic chunk positioning. It reduces state complexity while still letting the receiver reconstruct decryption parameters exactly.

2.4 Metadata stripping is an extra privacy layer, not the core cryptographic story

Before encryption, the frontend optionally strips removable metadata from supported image types such as JPEG, PNG, and WebP by redrawing them and re-exporting the pixels.

This solves a different privacy problem. Even if a file is later encrypted, an untouched photo may still contain EXIF data with location or device information. Xdrop deals with that by stripping metadata before the encrypted transfer is prepared.

3. The upload pipeline: how the browser chunks, encrypts, and resumes transfers

One of the strongest parts of Xdrop is not merely that it performs end-to-end encryption. It is that it turns the browser into a recoverable upload runtime rather than a one-shot upload form.

3.1 It prepares source files and estimates ciphertext size before upload begins

During upload preparation, the client does several things for each file:

  • generates an opaque fileId
  • creates a per-file noncePrefix
  • computes totalChunks from the configured chunk size
  • predicts each ciphertext chunk size as plaintext + 16 bytes for the GCM tag
  • sums the total encrypted transfer size in advance

That lets Xdrop enforce limits before it commits to the transfer. The current defaults are shared between the frontend and backend:

  • chunk size: 8 MiB
  • maximum file count: 100
  • maximum encrypted transfer size: 256 MiB

These values are conservative, but they fit the real design target: browser-side encryption with local persistence and resume support. Xdrop is not currently pretending to be a multi-terabyte archival pipeline. It is optimizing for a trustworthy encrypted handoff that remains understandable and robust in actual browser environments.

3.2 OPFS first, IndexedDB as fallback, so refreshes do not destroy the transfer

If a browser only uploads from the live File handle, a page refresh means starting over. Xdrop treats local source persistence as a first-class part of the runtime.

It prefers OPFS (Origin Private File System) for staged source files. If the browser does not support OPFS, or if fallback is needed, it can store Blob-backed file data in IndexedDB within a configured limit.

That produces a very practical recovery flow:

  • when the page is refreshed, in-flight uploads are marked as paused
  • when the page is reopened, the client reloads local transfer and source records
  • the client calls /resume to ask which chunks already exist
  • only missing chunks are uploaded again

That also shows that recovery is not an optional extra. It is part of the upload model itself.

3.3 Web Workers handle the heavy crypto while the main thread orchestrates

Xdrop pushes expensive operations into a dedicated worker:

  • wrapping and unwrapping the root key
  • encrypting and decrypting the manifest
  • encrypting and decrypting file chunks

That keeps React rendering and large cryptographic operations from fighting over the main thread. For users, the immediate benefit is a UI that remains responsive during large transfers. Architecturally, it also cleanly separates “cryptographic execution” from “application state.”

3.4 Upload URLs are presigned in batches rather than all at once

Xdrop does not presign PUT URLs for the entire transfer in a single shot. The frontend asks /upload-urls for chunks in batches, then uploads those chunks in parallel and reports them back through /chunks/complete with ciphertext size and SHA-256 checksum metadata.

That design has several advantages:

  • presigned URLs have a smaller exposure window
  • retries affect only a batch rather than the full transfer
  • the server can re-check state at each batch boundary

Once all chunks are acknowledged, the client:

  1. builds the manifest
  2. encrypts it with the rootKey
  3. wraps the rootKey with the linkKey
  4. uploads the manifest
  5. calls /finalize to move the transfer into ready

In other words, a transfer only becomes downloadable at the very end. Until that point, the public API treats it as incomplete.

4. The backend is intentionally narrow: it stores state, not meaning

The backend is written in Go, but the most important choice is not the language. It is the responsibility boundary. The server behaves much more like a transfer lifecycle coordinator than a traditional file server.

4.1 The API surface is centered on transfer lifecycle

The server-side API surface is intentionally tight:

  • create transfer
  • register files
  • create upload URLs
  • complete chunks
  • upload manifest
  • finalize transfer
  • resume, manage, and delete
  • public read of transfer status
  • public creation of download URLs

There is no endpoint that means “upload plaintext file” or “download plaintext file.” The server only knows that a transfer exists, that it contains opaque file IDs, that chunk indexes map to object keys, and that certain lifecycle steps have completed.

4.2 PostgreSQL stores a state graph, not file semantics

The database has three core tables:

  • transfers
  • transfer_files
  • transfer_chunks

The important fields include:

  • wrapped_root_key
  • manifest_object_key
  • manage_token_hash
  • opaque_file_id
  • chunk_index
  • checksum_sha256

Two implementation details matter a lot:

  • manage tokens are not stored in plaintext; the server stores a SHA-256 digest
  • file IDs are opaque client-generated identifiers, not filenames

That is the core backend design philosophy in Xdrop. The database needs to express transfer progress, but it does not need to know that a file is called contract.pdf.

4.3 The manage token is a sender-side control capability issued once

When a transfer is created, the API returns a manageToken. It is only revealed once in plaintext to the sender. From that point on, the server stores only its hash and validates it with constant-time comparison.

That token gives the sender a well-defined control surface:

  • resume an upload
  • extend expiry
  • delete a transfer

If the sender enables privacy mode, local control state is scrubbed after upload completes. The share link can still work, but local extend and delete controls disappear. This is an important nuance: it is not about making the cryptography stronger. It is about reducing residual sensitive state on the sender device.

4.4 Object storage is the ciphertext destination; the API mostly presigns

The storage layer is structured very clearly:

  • the API talks to a private S3-compatible endpoint
  • a different public endpoint can be used for externally visible presigned URLs
  • manifest and chunk payloads are just objects
  • cleanup operates by deleting the transfers/<id>/ prefix

The object layout is consistent:

transfers/<transferId>/manifest.bin
transfers/<transferId>/files/<fileId>/chunks/<chunkIndex>.bin

That means the backend is organized around object layout and lifecycle, not around interpreting file contents. For MinIO, S3, or any compatible storage system, Xdrop simply treats them as ciphertext chunk storage.

4.5 Security headers, rate limits, and cleanup jobs form the baseline defense

Xdrop does not act as though end-to-end encryption alone is the whole security model. The backend also adds operational defenses:

  • Redis-backed fixed-window rate limiting
  • origin allowlisting via ALLOWED_ORIGINS
  • baseline response hardening with headers such as X-Content-Type-Options, X-Frame-Options, and Cross-Origin-Opener-Policy
  • background cleanup jobs that purge expired or deleted transfer prefixes and then mark them as purged in the database

These mechanisms are not the cryptographic core, but they determine whether the service can be exposed safely and run reliably.

5. Downloads and agent integration: one protocol for humans and automation

If the upload side shows how Xdrop implements browser-side encryption, the download side shows how seriously it takes protocol unification across user types.

5.1 Receivers fetch state first, then the manifest, and only then decrypt locally

When a receiver opens /t/:transferId#k=..., the browser first calls the public descriptor endpoint. Only when the transfer is ready does the server return:

  • wrappedRootKey
  • manifestUrl
  • limited download configuration

The receiver then:

  1. uses the fragment linkKey to unwrap the wrappedRootKey
  2. fetches the encrypted manifest
  3. decrypts the manifest locally into a file list
  4. requests download URLs for the listed chunks

That means the public API only exposes ciphertext-oriented descriptors that are safe to expose. It never returns a plaintext file list or a usable decryption secret as ordinary JSON.

5.2 ZIP downloads are assembled locally, and folder structure comes from the manifest

For folder downloads, Xdrop does not ask the server to assemble a ZIP archive on demand. Instead, the browser decrypts files as streams and uses zip.js to build the ZIP locally.

That does more than remove backend complexity. It preserves a larger principle: the server delivers encrypted objects, not reconstructed plaintext artifacts. Directory paths come from the decrypted manifest, and ZIP structure is rebuilt on the client.

The server never needs to know that a certain set of chunks belongs to docs/readme.txt. It only needs to make the corresponding encrypted chunks retrievable.

5.3 Agent workflows reuse the exact same share format

One of the strongest product decisions in Xdrop is that agent support is not implemented as a second, separate API line. The companion Xdrop skill and command-line workflow revolve around the same capabilities:

  • upload a local file or directory
  • return the full encrypted share link
  • use a full link with #k= for local decryption download

That makes Xdrop a useful end-to-end encrypted handoff channel for remote servers, containers, and CI-adjacent environments. An agent can send logs, artifacts, diagnostic bundles, or outputs back to a local user without forcing the server to act as a holder of plaintext file semantics.

That is what “for humans and agents” means at the architecture level, not just at the homepage level.

6. Engineering tradeoffs: why Xdrop fits trustworthy temporary handoffs better than a general cloud drive

Xdrop is thoughtfully designed, but it does not try to become an everything-platform. Many of its limits are there to keep the boundaries clear.

6.1 The current defaults reveal the intended problem space

The default 8 MiB chunk size, 100 file cap, and 256 MiB encrypted transfer limit show what Xdrop is optimizing for:

  • temporary sharing
  • human-to-human and agent-to-human handoffs
  • browser-friendly local persistence and recovery

If you need terabyte-scale archival storage, layered permissions, team workspaces, or deep audit features, that is not the core Xdrop problem domain today.

6.2 “No account” and “local history only” are simplifications, but also boundaries

Xdrop very clearly chooses:

  • no account system
  • no cross-device history synchronization
  • sender-side management records only in the current browser

That makes the system lighter and narrows the trust surface. The tradeoff is that if you lose local state, or if privacy mode scrubs local controls after upload, manageability decreases. That is not an accident. It is part of the product’s commitment to minimal server state and short-lived trusted context.

6.3 The deployment model favors self-hosting with explicit boundaries

In the default Docker deployment, the final container combines:

  • nginx for the static frontend
  • the Go API

alongside:

  • PostgreSQL
  • Redis
  • MinIO

A safer deployment pattern is:

  • exposing only a reverse proxy to the public Internet
  • binding the xdrop container and MinIO privately when possible
  • rebuilding the frontend image if you want production-correct VITE_SITE_URL and canonical metadata

This is not a flashy cloud-native demo, but it is a very practical shape for personal, small-team, and self-hosted deployments. The topology is simple, the boundaries are easy to reason about, and the operational story matches the trust model.

Closing Thoughts

What makes Xdrop worth studying is not that it uses the phrase “end-to-end encrypted.” It is that the system actually carries that promise through the implementation:

  • #k= fragments keep decryption capability client-side
  • HKDF plus AES-256-GCM create a clean, scoped cryptographic model
  • Web Workers, OPFS, and IndexedDB turn the browser into a resumable transfer runtime
  • a Go API, PostgreSQL, Redis, and S3-compatible storage form a backend that manages lifecycle rather than plaintext
  • one share protocol serves both human browser flows and agent-driven terminal workflows

Xdrop is not trying to be a universal file platform. But for the problem of trustworthy, temporary, encrypted handoffs across human and machine boundaries, it already shows strong product judgment and solid engineering discipline. In an era of remote development, containers, and agent-assisted workflows, that is a much more interesting design target than a generic upload page.