It was a rainy autumn evening, the kind where you should probably be reading a book or doing something relaxing. Instead, I was wondering if one could build a pastebin without a server.
So I built CopyPasta, a tiny app that compresses text into shareable URLs. Everything happens in the browser. No server, no database, no backend at all.
The idea is to encode the text in the URL itself. Sounds simple, but there’s a problem: URLs have practical length limits (around 2000 characters for broad compatibility), and even a few paragraphs of text can blow past that quickly.
That’s where compression comes in. Modern browsers have a fantastic API that I didn’t know existed until I started this project: the Compression Streams API. It lets you gzip compress and decompress data entirely in the browser, using streams.
Here’s the core compression function look like:
async function compressText(text) {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(text));
controller.close();
},
});
const compressed = stream.pipeThrough(new CompressionStream("gzip"));
const chunks = [];
const reader = compressed.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
// Combine chunks into a single Uint8Array
const result = new Uint8Array(chunks.reduce((acc, c) => acc + c.length, 0));
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
The text gets encoded to bytes, piped through a gzip compression stream, and collected into a Uint8Array. Decompression works the same way in reverse, using DecompressionStream.
Once the text is compressed, it needs to fit into a URL. The compressed data is binary, so I needed a way to represent it as text. Base64 encoding can be a simple solution for this. It converts binary data into ASCII characters that are safe to use in URLs.
With a few adjustments for URL safety:
const arrayBufferToBase64 = buf =>
btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
Standard Base64 uses + and /, which can cause issues in URLs, so they’re replaced with - and _. The padding = characters at the end are also stripped since they can be reconstructed when decoding.
The compressed data goes into the URL fragment (the # part), which is never sent to the server. So even if someone hosts this tool, they’d never see what users are sharing. That’s the “100% Private” part.
Compression rates vary wildly depending on the content. Repetitive text compresses beautifully (sometimes by 80% or more). Random strings or already compressed data not so much.
In some cases, the gzip overhead can even make the output slightly larger than the input.
The entire project is a single HTML file. No build process, no npm packages, no framework. Just HTML, CSS, and vanilla JavaScript. You can open it locally or drop it on any static host.
The source code is on GitHub, and you can try it here. It’s public domain (Unlicense), so feel free to host your own instance or modify it however you like.
If you’re curious about what else browsers can do natively these days, I highly recommend poking around the Web APIs documentation. There’s a lot more built-in functionality than you might expect.
[Update 2025-11-16]
Base64 has a 33% overhead as it encodes 3 bytes as 4 characters. For a URL-based tool, that’s not ideal. There are more URL-safe characters available than Base64 uses (64 characters). In theory, you could design an encoding that uses more of the URL safe character set to reduce overhead. For example, encoding 4 bytes into 5 characters instead of 3 bytes into 4 would improve the ratio.
I haven’t implemented this, but it’s an interesting optimization to explore. The trade-off would be more complex encoding/decoding logic versus slightly shorter URLs. For most use cases, the Base64 overhead is acceptable, but if you’re regularly hitting URL length limits, a more efficient encoding could make a difference.
If someone wants to give it a try, let me know :)