René's Blog

Building PlainTray: Zero-Knowledge File Sharing That Just Works

Sharing a file securely in 2026 is still surprisingly annoying. Email attachments have size limits and sit unencrypted on some server. WeTransfer is convenient but they can read everything you upload. Dropbox and Google Drive require accounts on both ends. And self-hosted solutions are great until you realize you’re now responsible for keeping a server patched and running.

I first tried to solve this back in 2014 with a Java web app I built at university. It worked well — a bit too well, actually. More on that later. Fast forward to 2026, I rebuilt the whole thing from scratch with modern crypto primitives and edge infrastructure. Meet PlainTray.

The 30-Second Version

Here’s how it works:

  1. Visit plaintray.com — a new tray code is generated automatically
  2. Type text in the editor or drop files and folders into the upload zone
  3. Share the short URL (like plaintray.com/#a3Da93)
  4. Anyone with the link opens it and sees the content immediately

No account, no app install, no configuration. Everything is end-to-end encrypted — the server only ever sees ciphertext. Multiple people can even edit the same tray simultaneously with real-time collaboration.

The free tier gives you 5 GB of storage and 7 days of retention. That’s enough for most quick sharing scenarios. Try it at plaintray.com.

The URL Fragment Trick

The entire security model starts with a single character: #.

When you open plaintray.com/#a3Da93, the part after the hash — a3Da93 — is called the URL fragment. Here’s the thing about URL fragments: browsers don’t send them to the server. Ever. It’s part of the HTTP spec. The server receives a request for plaintray.com/ and nothing else.

This means the server literally cannot see your tray code. It’s not a policy decision or a pinky promise — it’s how HTTP works. That one character makes the entire architecture trustless.

From Code to Crypto Key

So the server never sees the code. But we still need the server to look up the right tray, and we need an encryption key to protect the content. The trick is deriving both from the same code, client-side.

Here’s a simplified version of the actual derivation from Keys.ts:

const SALT = 'plaintray_v1';
const ITERATIONS = 6_123_456;
const KEY_LENGTH_IN_BYTES = 48; // 16 for tray ID + 32 for AES key

const deriveKeys = async (code: string) => {
  const enc = new TextEncoder();
  const baseKey = await crypto.subtle.importKey(
    'raw', enc.encode(code), { name: 'PBKDF2' }, false, ['deriveBits']
  );
  const derived = await crypto.subtle.deriveBits(
    { name: 'PBKDF2', hash: 'SHA-256', salt: enc.encode(SALT), iterations: ITERATIONS },
    baseKey,
    KEY_LENGTH_IN_BYTES * 8
  );
  const fullKey = new Uint8Array(derived);
  const id = toHex(fullKey.slice(0, 16));   // tray ID — sent to server
  const key = fullKey.slice(16);             // AES-GCM key — never leaves browser
  return { id, key };
};

PBKDF2 with 6.1 million iterations takes about 2.5 seconds on a typical device. That’s a deliberate design choice: it’s a barely noticeable pause for the legitimate user but makes brute-forcing the 6-character code space expensive. An attacker would need ~2.5 seconds per guess, and there are roughly 38 billion possible codes (58^6). On top of that, the server has rate limiting — even if someone tried to guess tray IDs by hitting the API, they’d get shut down quickly.

The first 16 bytes of the derived output become the tray ID — a hex string the server uses to look up the encrypted data. The remaining 32 bytes become the AES-GCM encryption key, which never leaves the browser. Clean split: the server knows where your data is but can never read what it is.

Tradeoffs I Made on Purpose

Zero-knowledge encryption with a short code involves real tradeoffs. I want to be upfront about them.

Fixed salt. The PBKDF2 salt is the same for every tray. Normally you’d want a unique salt per derivation. But here, the code needs to deterministically produce the tray ID — without a server round-trip asking “what salt should I use for this tray?” A per-tray salt would break the zero-knowledge property before we even get to encryption.

38 billion codes, not infinite. A 6-character code from a 58-character alphabet gives about 38 billion possibilities. Collisions are theoretically possible but negligible at the scale PlainTray operates at. If you need a bigger namespace, the Pro tier will offer 12-character codes (~1.4 x 10^21 combinations).

One code = full access. If someone has the code, they can read and write. You can’t revoke access without changing the code, which means a new tray. That’s the cost of simplicity — there are no permissions, no roles, no “view only” links. For the use case of quick, temporary sharing, this is a trade I’m comfortable with. The Pro tier will add read-only permissions for more granular control when you need it.

7-day expiry as a safety net. Free trays auto-expire after a week. This limits the window for any potential enumeration attack and keeps the platform clean. It also means people don’t leave sensitive data sitting around forever.

None of these are oversights. They’re conscious decisions that prioritize simplicity for the 99% use case.

Real-Time Collaboration on Encrypted Data

One of the features I’m most happy with is real-time collaboration. Multiple people can open the same tray and edit the text area simultaneously — and it all stays encrypted.

This works by combining Yjs (a CRDT library for conflict-free collaborative editing) with client-side encryption. Every Yjs update is encrypted with the tray’s AES-GCM key before it’s sent over the WebSocket connection. The relay on the server side is a Cloudflare Durable Object that forwards messages between connected clients. It never decrypts anything — it’s just passing opaque bytes around.

The result: Google Docs-style collaboration, but the server is cryptographically locked out of the content.

The Stack

  • SvelteKit with Svelte 5 runes for the frontend
  • Cloudflare Workers for the API (no origin server, everything runs at the edge)
  • D1 (Cloudflare’s SQLite-at-edge database) for tray metadata and encrypted text
  • R2 (Cloudflare’s object storage) for encrypted file uploads
  • Durable Objects for the WebSocket relay that powers real-time collaboration
  • Paraglide JS for i18n — PlainTray ships in 8 languages from day one

Everything runs on one platform. No separate database server, no Redis instance, no container orchestration. A daily cron job cleans up expired trays, and R2 lifecycle rules act as a safety net for orphaned files. Ops overhead is near zero.

From a University Java App to Edge Computing

The original version of this idea was a Java web app I built in 2014, during university. Simple concept — upload files, get a link, share it. It worked, and people loved it. Word spread around campus, usage grew.

And then I shut it down.

The reason? The server had about 50 GB of storage, and it kept filling up. Instead of figuring out how to scale it — adding more disk space, implementing cleanup routines, finding budget for infrastructure — I just pulled the plug. Not my proudest product decision.

The irony is obvious: I killed my own product because people were actually using it. Classic.

Fast forward twelve years, and the economics have completely changed. Cloudflare R2 gives you practically infinite object storage at commodity prices. The Web Crypto API means real encryption runs natively in the browser. Edge computing means I don’t even need to think about server regions. What was impossible on a single university machine is now trivial on modern infrastructure.

So I rebuilt it. This time with actual encryption, automatic expiry, real-time collaboration, and infrastructure that scales to whatever demand shows up.

Give It a Try

PlainTray is live and free to use — no sign-up required. Open it, drop some files or type some text, share the link.

A Pro tier is coming soon at $7/month with 30-day retention, password protection, and a tray dashboard. But the free tier isn’t going anywhere.

If you want to see how it works under the hood, the How It Works page has a visual walkthrough, and the About page has more on the origin story.

Have a nice day!