When the Email Is Too Big
How a self-hosted, encrypted file-drop built with Gokapi, Cloudflare and OCI Object Storage rescued an application document that got silently swallowed by a mail relay — and two things that surprised me while building it.
The Problem: The Email Never Arrived
My outbound email runs through a cloud relay service — convenient, reliable, but with a limitation I learned the hard way: a hard cap of 2 MB per message, attachments included. Above that limit, the thing that happens isn't what you'd expect (a clear error message) — it's worse: the connection is simply dropped mid-transfer, silently. No bounce, no error — just a mail sitting in the queue for days until a vague "delayed" warning eventually shows up.
This happened to me with an important job-application document, with a real deadline attached. Not a server problem in the classic sense, but a wake-up call: for anything over a few megabytes, I needed a different path.
The Overview, Before It Gets Technical
In short: instead of a file attached to the email, the recipient gets only a short link. The actual file sits encrypted in object storage, the server in between isn't directly reachable by outsiders, and the link expires automatically after a short time.
The Options — and Why I Didn't Just Take One
The obvious solution would have been any random cloud-storage link. For sensitive documents, though, I wanted something that satisfied three properties at once:
- I control the data — no third-party cloud whose access terms I have to trust.
- Encrypted, more than once — both at the storage layer and at the application layer, not just "the provider promises encryption".
- A short, professional link — the recipient (in this case an HR contact) should be able to just click, no account, no key exchange.
The answer was Gokapi, a lightweight open-source tool for temporary, expiring download links, hidden behind Cloudflare, with Oracle Cloud Infrastructure (OCI) Object Storage behind it.
The Architecture in Short
Recipient ── https ──▶ Cloudflare (TLS termination,
rate limiting, cache bypass for downloads)
│ origin pull only via mTLS —
│ only Cloudflare itself gets through
▼
Origin server
┌───────────────────────────────────────────┐
│ nginx — verifies the mTLS client cert to │
│ make sure the request really comes from │
│ Cloudflare, not from anywhere else │
│ Gokapi (Docker) │
│ • only metadata lives on the server │
│ • file bodies → OCI Object Storage, │
│ encrypted (provider key + Gokapi's own │
│ application-level encryption) │
└───────────────────────────────────────────┘
A few design decisions I think are worth sharing:
The origin server isn't directly publicly reachable — mTLS instead of
an IP filter. The obvious instinct would have been a firewall rule
allowing only Cloudflare's IP ranges through. That's more fragile in practice
than it sounds — nginx rewrites the "true" client IP before the actual
access check runs, so a simple allow/deny rule
suddenly tests the wrong address and blocks legitimate requests instead of
malicious ones. The more robust fix is a client certificate that only
Cloudflare holds (Authenticated Origin Pulls) — the origin verifies the
certificate cryptographically, regardless of which IP is knocking.
Double encryption, not just "trust the provider". OCI Object Storage encrypts with its own (not the default) key, and on top of that Gokapi itself encrypts every file before it's even uploaded. Two independent layers instead of one.
Downloads go through Gokapi, not a direct link to the storage backend. The obvious shortcut — redirecting the client straight to a temporary storage URL — would have saved some bandwidth on the server, but at two costs: the recipient would briefly see an ugly, technical URL instead of a clean link, and the server-side "delete after X downloads" logic would be easier to bypass. So every download goes through Gokapi itself.
What Actually Happens on a Download
The diagram above shows the stations, not what actually happens on each click. Step by step:
- 1. Upload. I upload the file to Gokapi in the browser. Gokapi encrypts it immediately, server-side, before a single byte heads toward object storage — the storage backend never sees the plaintext file.
- 2. Store. The encrypted file lands in OCI Object Storage. The server itself only keeps a bit of metadata: a short, random ID, expiry date, remaining downloads, optionally a password hash. The file itself never touches the server's disk.
- 3. Share the link. The link I paste into the email contains only that ID — not the file, and no key. Without a valid, still unexpired entry in Gokapi's own database, the link simply doesn't work.
- 4. Recipient clicks. The request goes to Cloudflare first. Cloudflare fetches the response from the origin server — but only because it identifies itself with its own client certificate (the mTLS mentioned above). Any request that skips this detour through Cloudflare gets rejected by the origin server before any content is served at all.
- 5. Deliver. Gokapi fetches the encrypted file from object storage itself and streams it to the recipient's browser — no redirect to a separate storage address, everything travels the same, locked-down path. Only in the recipient's browser does the file get decrypted, right before it's saved. Gokapi then counts the download; once the limit is reached, the same link stops serving anything on the next attempt.
At no point in this chain does the unencrypted file sit anywhere longer than the processing at that step actually requires — and without Cloudflare's certificate, nobody even gets past step 5.
What Surprised Me While Building It
The architecture on paper was the easy part. A few details caught me off guard during the actual setup — exactly the kind of thing no tutorial mentions:
Out of habit, I set restrictive security headers on every origin server,
including one that prevents the page from being embedded in an
<iframe> — a standard clickjacking defense. Gokapi's
download client, however, uses exactly such an iframe internally (for a
service worker that handles decryption in the browser) — same-origin,
technically harmless, but my header blocked it anyway. The download hung
forever, with no error on the server at all. Found only via the browser
console, not through any server log. The lesson: "restrictive by
default" isn't a universal answer — you have to understand what your own
application actually needs before cranking every header to the strictest
setting.
After creating the new DNS record, the TLS connection failed for several minutes — exactly the picture you'd expect from a freshly issued certificate still propagating. The actual cause was simpler, and wouldn't have resolved itself no matter how long I waited: Cloudflare's "Universal SSL" base feature for automatic certificate issuance was simply turned off. Without it, Cloudflare never issues a certificate at all, no matter how long you wait. The lesson here: "still propagating" is a tempting but not always correct first diagnosis — worth checking the basic settings before you go get coffee.
Conclusion
The actual build — spinning up the container, wiring up storage, configuring Cloudflare — took an afternoon. The surprises above took longer combined than the rest of it, and none of them were in the official documentation of the tools involved. That's ultimately why I'm writing this up: not as a "here's how to build a file-drop" tutorial, but as a collection of the spots where reality diverges from the docs — and where looking closer paid off.
I missed the original deadline in the end anyway — but the file-drop works now, the document got sent, and I'm set up for next time.
For specific questions about configuration, deployment, or your own file-drop setup, reach me via the contact page.
Tools & Projects Used
- Gokapi — self-hosted file-drop with built-in encryption and expiring download links (AGPL-3.0)
- Cloudflare — DNS, TLS termination, Authenticated Origin Pulls (mTLS)
- nginx — reverse proxy and mTLS enforcement at the origin
- Docker — container runtime for Gokapi
- Oracle Cloud Infrastructure (OCI) Object Storage — encrypted object storage for the file bodies