Last Updated: February 2026
This document describes what data Dropgate collects, where and why it is stored, how it is processed, and when it is deleted. It covers all three components of the monorepo: the Dropgate Server, the Dropgate Client (Electron), and the dropgate-core library (which is also used in the Web UI).
Dropgate follows a data-minimisation approach:
- No user accounts or authentication. There is no concept of a registered user. No usernames, passwords, email addresses, or tokens are collected.
- No tracking or analytics. Dropgate does not embed analytics scripts, tracking pixels, or third-party telemetry.
- No cookies. Neither the server nor the Web UI sets any cookies.
- No persistent client-side web storage. The Web UI does not use
localStorage,sessionStorage, orIndexedDB. - Encryption by default. When E2EE is enabled (the default), the server stores only ciphertext and has no mechanism to recover plaintext file content or filenames.
The following tables enumerate every category of data processed by Dropgate, grouped by component.
| Data | Stored? | Where | Why | When Created | When Deleted |
|---|---|---|---|---|---|
| File content (plaintext or ciphertext) | Yes | Filesystem: /uploads/<fileId> |
Core purpose — the file must be persisted so recipients can download it. | On upload completion (/upload/complete). |
On expiry, max downloads reached, or server restart (unless UPLOAD_PRESERVE_UPLOADS=true). |
| Temporary file (partial chunks) | Yes | Filesystem: /uploads/tmp/<uploadId> |
Chunks must be written to disk as they arrive; holding them in memory would be infeasible for large files. | On upload initialisation (/upload/init). |
On upload completion (renamed), cancellation, zombie cleanup, or server restart. |
| Filename | Yes | Database (in-memory or SQLite) | Required to set Content-Disposition on download. For encrypted uploads, the stored value is a Base64-encoded ciphertext blob — the server cannot read it. |
On upload completion. | When the file record is deleted. |
| File size (bytes) | Yes | Database | Used for storage quota accounting, Content-Length headers, and progress reporting to download clients. |
On upload completion. | When the file record is deleted. |
Encryption flag (isEncrypted) |
Yes | Database | Determines how the server serves the file (headers, MIME type, secure-context enforcement). | On upload completion. | When the file record is deleted. |
| Download count | Yes | Database | Enforces the maximum-download limit. Only stored when maxDownloads > 0. |
On first download. Incremented on each subsequent download. | When the file record is deleted. |
| Maximum downloads | Yes | Database | Reference value for the download-count check. | On upload completion. | When the file record is deleted. |
Expiry timestamp (expiresAt) |
Yes | Database | Drives automatic deletion. null if no expiry. |
On upload completion. | When the file record is deleted. |
| File ID (UUID) | Yes | Database (as key) | Unique identifier used in download URLs. | On upload completion. | When the file record is deleted. |
| Upload ID (UUID) | Temporarily | In-memory (ongoingUploads Map) |
Tracks the upload session whilst chunks are being received. | On upload initialisation. | On completion, cancellation, zombie cleanup, or server restart. |
| Received chunk indices | Temporarily | In-memory (Set within upload session) | Detects duplicate chunks and validates completeness. | On each chunk upload. | When the upload session ends. |
| Reserved storage bytes | Temporarily | In-memory (quota counter) | Prevents TOCTOU race conditions during concurrent uploads. | On upload initialisation (under mutex). | Released on completion, cancellation, or zombie cleanup. |
| Chunk hash (SHA-256) | No | — | Verified on receipt and discarded. Not persisted. | — | — |
| Data | Stored? | Where | Why | When Created | When Deleted |
|---|---|---|---|---|---|
| Bundle ID (UUID) | Yes | Database (as key) | Unique identifier for the bundle download URL. | On bundle completion. | On expiry or max downloads reached. |
| Encrypted manifest (sealed bundles) | Yes | Database (Base64 blob, ≤ 1 MiB) | Allows the download client to enumerate files in the bundle. The server stores it as an opaque blob and cannot read it. | On bundle completion. | On expiry or max downloads reached. |
| Plaintext file list (unsealed bundles) | Yes | Database (JSON array of { fileId, name, sizeBytes }) |
Allows the server to serve the bundle download page and enumerate member files. | On bundle completion. | On expiry or max downloads reached. |
| Bundle upload ID (UUID) | Temporarily | In-memory (ongoingBundles Map) |
Tracks the bundle session whilst individual files are being uploaded. | On bundle initialisation. | On bundle completion or zombie cleanup. |
| Data | Stored? | Where | Why | When Created | When Deleted |
|---|---|---|---|---|---|
| Peer IDs (P2P codes) | Transiently | PeerJS in-memory (not Dropgate-managed) | Peer discovery and routing. | On peer registration. | On peer disconnection. |
| ICE candidates | Transiently | PeerJS in-memory (not Dropgate-managed) | NAT traversal — relayed between peers during WebRTC connection setup. Contains IP addresses and ports. | During ICE gathering. | On connection establishment or failure. |
| SDP offers/answers | Transiently | PeerJS in-memory (not Dropgate-managed) | WebRTC session negotiation. | During connection setup. | On connection establishment or failure. |
| File content | Never | — | File data flows directly between peers via the WebRTC data channel. The server is not involved. | — | — |
| File metadata (name, size, MIME) | Never | — | Exchanged between peers over the encrypted data channel. The server cannot observe it. | — | — |
| Data | Stored? | Where | Why | When Created | When Deleted |
|---|---|---|---|---|---|
| Client IP address | Transiently | In-memory (rate limiter) | Rate limiting. Tracked per sliding window by express-rate-limit. Not written to disk or database. |
On each HTTP request. | When the rate-limit window expires (default: 60 seconds). |
| User-Agent header | No | — | Present in HTTP requests but not logged, stored, or processed by Dropgate. | — | — |
| Request paths and methods | No (unless logging) | stdout/stderr (if LOG_LEVEL ≥ INFO) |
Operational logging. Not structured or persisted by Dropgate itself. Persistence depends on the server operator's log infrastructure. | On relevant server events. | Determined by operator's log retention policy. |
| Data | Stored? | Where | Why | When Created | When Deleted |
|---|---|---|---|---|---|
| Server URL | Yes | electron-store (config.json in user data directory) |
Remembers the user's preferred server between sessions. | On user input (settings form). | On user change or application uninstall. |
| Lifetime preference (value + unit) | Yes | electron-store |
Remembers the user's preferred file lifetime. | On user input. | On user change or application uninstall. |
| Max downloads preference | Yes | electron-store |
Remembers the user's preferred download limit. | On user input. | On user change or application uninstall. |
| Window bounds (x, y, width, height) | Yes | electron-store |
Restores window position and size between sessions. | On window move/resize. | On application uninstall. |
| Debug log | Yes | {userData}/debug.log |
Troubleshooting application issues. Contains timestamps, process arguments, app lifecycle events, and upload progress. Does not contain file content or encryption keys. | On application start. | Manual deletion by user. |
| Data | Stored? | Where | Why | When Created | When Deleted |
|---|---|---|---|---|---|
| Server capabilities | Transiently | JavaScript memory | Cached /api/info response for the current page session. |
On connection test. | On page unload. |
| File references | Transiently | JavaScript memory (File objects) |
The user's selected files, held in memory for upload. | On file selection. | On page unload or upload completion. |
| Transfer progress | Transiently | JavaScript memory | Percentage, bytes transferred, etc. | During upload/P2P transfer. | On page unload or transfer completion. |
When E2EE is active:
- File content — encrypted with AES-256-GCM before leaving the client.
- Filenames — encrypted with the same key and a separate IV.
- Bundle manifests (sealed bundles) — encrypted client-side; the server stores an opaque blob.
Even with E2EE active, the following metadata is visible to the server:
- File size (including encryption overhead).
- Number of chunks.
- Whether the file is encrypted (
isEncryptedflag). - Expiry timestamp and download limits.
- Upload timing patterns (when chunks arrive).
- Generated by the client using
crypto.subtle.generateKey. - Used to encrypt all chunks and the filename.
- Exported to URL-safe Base64 and appended to the download URL as a fragment (
#<keyBase64>). - Never transmitted to the server. URL fragments are not included in HTTP requests.
- Not persisted by the client. The key exists only in the download link. If the link is lost, the file cannot be decrypted.
The server has no access to encryption keys and therefore cannot:
- Decrypt file content.
- Read original filenames (when encrypted).
- Read sealed bundle manifests.
- Recover keys from stored data.
/uploads/ Main upload directory
├── <fileId> Completed files (UUID names, no extensions)
├── tmp/
│ └── <uploadId> Temporary files during upload
└── db/ Only if UPLOAD_PRESERVE_UPLOADS=true
├── file-database.sqlite
└── bundle-database.sqlite
| Structure | Contents | Lifetime |
|---|---|---|
ongoingUploads (Map) |
Active upload sessions. | Until completion, cancellation, or zombie cleanup. |
ongoingBundles (Map) |
Active bundle sessions. | Until bundle completion or zombie cleanup. |
| Rate limiter store | IP → request count mappings. | Sliding window (default 60 s). |
| File/bundle database | File/bundle metadata. | Persistent (SQLite) or until restart (in-memory). |
| PeerJS state | Peer connections, ICE candidates, SDP. | Until peer disconnection. |
UPLOAD_PRESERVE_UPLOADS |
Database Driver | Behaviour on Restart |
|---|---|---|
false (default) |
In-memory | All metadata lost. All files in /uploads/ deleted. |
true |
SQLite (/uploads/db/) |
Metadata and files preserved. Only /uploads/tmp/ is cleaned. |
| Trigger | What Is Deleted | Frequency |
|---|---|---|
File expiry (expiresAt < now) |
File from disk + database record. | Checked every 60 seconds. |
| Bundle expiry | Sealed: manifest record. Unsealed: all member files + manifest. | Checked every 60 seconds. |
| Max downloads reached | Single file: file + record. Unsealed bundle: all member files + manifest. Sealed bundle: manifest record only. | Immediately after the triggering download. |
| Zombie upload cleanup | Temporary file + storage reservation + session state. | Every 5 minutes (configurable via UPLOAD_ZOMBIE_CLEANUP_INTERVAL_MS). |
| Server restart (non-persistent mode) | All files, temporary files, and in-memory data. | On process start. |
| Server restart (persistent mode) | Only temporary files in /uploads/tmp/. |
On process start. |
| Action | What Is Deleted |
|---|---|
Upload cancellation (POST /upload/cancel) |
Temporary file, storage reservation, session state. |
P2P transfer cancellation (either peer calls stop()) |
Connection resources. No server data to delete (DGDTP stores nothing on the server). |
- Electron client settings (
electron-store) — persist until the user changes them or uninstalls the application. - Electron debug log — persists until manually deleted by the user.
- Server operator logs (stdout/stderr) — Dropgate has no control over log retention once data is written to the process output streams. This is the operator's responsibility.
Client Server Filesystem
│ │ │
│ 1. POST /upload/init │ │
│ { filename, size, ... } │ │
│──────────────────────────────►│ │
│ │ 2. Create temp file │
│ │──────────────────────────────►│
│ │ 3. Reserve quota (mutex) │
│ │ │
│ 4. POST /upload/chunk [×N] │ │
│ <binary> │ │
│──────────────────────────────►│ │
│ │ 5. Verify SHA-256 │
│ │ 6. Write to temp file │
│ │──────────────────────────────►│
│ │ │
│ 7. POST /upload/complete │ │
│──────────────────────────────►│ │
│ │ 8. Rename temp → final │
│ │──────────────────────────────►│
│ │ 9. Write DB record │
│ │ 10. Update quota counter │
│ │ │
Data at rest: /uploads/<fileId> (ciphertext if E2EE)
Database record: filename, size, expiry, download count
Sender Signalling Server Receiver
│ │ │
│ 1. Register peer │ │
│ (P2P code) │ │
│───────────────────────►│ │
│ │ │
│ │ 2. Connect to code │
│ │◄─────────────────────────│
│ │ │
│ 3. ICE + SDP relay │ │
│◄══════════════════════►│◄════════════════════════►│
│ │ │
│ 4. Data channel established (DTLS-encrypted) │
│◄═════════════════════════════════════════════════►│
│ │ │
│ 5. File data [×N] │ │
│ (direct, not via │ Server NOT involved │
│ server) │ from this point │
│───────────────────────────────────────────────────►│
│ │ │
Data at rest: NONE (server stores nothing)
Data in transit through server: ICE candidates (IP:port), SDP, peer IDs
Data in transit peer-to-peer: File content + metadata (DTLS-encrypted)
| Level | Value | What Is Logged |
|---|---|---|
NONE |
-1 | Nothing. |
ERROR |
0 | Errors only (file I/O failures, unhandled exceptions). |
WARN |
1 | Warnings (deprecated features, recoverable issues). |
INFO |
2 | Operational events: upload init, completion, deletion, rate limiting, capacity. Default. |
DEBUG |
3 | Detailed diagnostics: chunk-level progress, validation rejections, size calculations. |
At INFO level (default):
- Server startup configuration (port, enabled features, limits).
- Upload lifecycle events: "Initialised upload", "File received", "File expired".
- Storage capacity changes.
- Rate limit triggers.
- Bundle creation and deletion.
At DEBUG level:
- Chunk-level reception details (index, size).
- Validation rejection reasons.
- Storage quota calculations.
At any log level, Dropgate does not log:
- File content.
- Encryption keys.
- Plaintext filenames (encrypted filenames appear as Base64 blobs).
- Individual client IP addresses (the rate limiter tracks these in memory, but they are not written to log output by Dropgate).
- Download URLs.
- User-Agent strings.
When PEERJS_DEBUG=true, the PeerJS library outputs its own debug information to the console. This may include peer IDs (P2P codes) and connection state details. Do not enable this in production unless actively debugging P2P connectivity issues.
Dropgate writes logs to stdout/stderr. Whether these logs are persisted, rotated, or forwarded to external systems is entirely under the server operator's control. Operators SHOULD:
- Set
LOG_LEVELto the minimum necessary for operational needs. - Implement log rotation to avoid unbounded log growth.
- Consider the sensitivity of
DEBUG-level output before enabling it. - Be aware that reverse proxy access logs (e.g., Nginx, Caddy) may capture client IP addresses, request paths, and file IDs even if Dropgate itself does not log them.
During DGDTP connection establishment, STUN binding requests are sent to the configured STUN server(s). These requests contain the sender's and receiver's IP addresses. The default STUN server is stun:stun.cloudflare.com:3478.
Mitigation: Self-host a STUN server to eliminate third-party IP exposure. Alternatively, use a VPN to mask real IP addresses before STUN requests are sent.
A TLS-terminating reverse proxy (Nginx, Caddy, etc.) sits between clients and the Dropgate Server. It sees:
- Client IP addresses.
- Request URLs (including file IDs, but not URL fragments containing encryption keys).
- Request and response sizes.
- TLS handshake metadata (SNI, client hello).
Mitigation: Configure the reverse proxy to minimise access logging. Be aware that file IDs in URLs are sufficient to construct download links (though not to decrypt encrypted files without the key fragment).
- Use non-persistent mode (
UPLOAD_PRESERVE_UPLOADS=false) unless persistence is required. This ensures all data is cleared on server restart, reducing the window of exposure for data at rest. - Set conservative lifetimes and download limits. Shorter lifetimes (
UPLOAD_MAX_FILE_LIFETIME_HOURS) and low download counts (UPLOAD_MAX_FILE_DOWNLOADS=1) minimise the duration and accessibility of stored data. - Restrict storage quota. A bounded
UPLOAD_MAX_STORAGE_GBlimits the volume of data that can accumulate. - Keep
LOG_LEVELatINFOor lower in production.DEBUGlogging includes chunk-level details that, in aggregate, reveal transfer patterns. - Disable
PEERJS_DEBUGin production. PeerJS debug output may include peer IDs and connection metadata. - Audit reverse proxy logs. The reverse proxy may capture data that Dropgate itself does not log. Apply appropriate retention and access controls.
- Review the
P2P_STUN_SERVERSconfiguration. If IP privacy is a concern, self-host STUN infrastructure rather than relying on third-party servers.
- Enable E2EE wherever possible. When encryption is active, the server cannot access file content or filenames. There is no meaningful performance cost.
- Share download links through secure channels. The encryption key is embedded in the URL fragment. Anyone with the full URL can decrypt the file.
- Use single-download mode (
maxDownloads=1) for sensitive files. The file is automatically deleted after one download, minimising exposure. - Use short lifetimes for sensitive files. Even if the download limit is not reached, the file will be automatically deleted when the lifetime expires.
- Consider using a VPN when connecting to a Dropgate Server. This is particularly relevant for P2P transfers (DGDTP), where ICE candidates can expose real IP addresses. Choose a VPN provider that supports peer-to-peer traffic and research their privacy policies, logging practices, and jurisdiction carefully.
- Prefer P2P (DGDTP) for the highest privacy. When both sender and receiver are online simultaneously, DGDTP transfers file data directly between peers without it ever touching the server. The server's role is limited to initial signalling.