Personal cross-device clipboard — paste on your phone, open a short link on your laptop. Friendly URLs, optional E2E encryption, expiry, syntax highlighting. React + Hono on Cloudflare Workers.
A personal cross-device clipboard. Paste a snippet on your phone, get a short
friendly link (like slip.you.workers.dev/swift-otter), and open it on your
laptop. Built for the "copy an error log on my Android phone → read it on my PC"
workflow, without bouncing it through a chat app.
- Friendly short links — two readable words, easy to type on the other device.
- QR code — on the result card, for instant phone-to-phone handoff.
- Per-paste expiry — 1 hour, 1 day, 1 week, or never. Handled natively by KV.
- Unguessable mode — a toggle that mints a long random link for sensitive text.
- End-to-end encryption (optional) — set a passphrase and the text is encrypted in your browser (AES-256-GCM) before upload; the server only ever stores ciphertext. The reader enters the passphrase to decrypt, also in-browser.
- Syntax highlighting — pick a language (or let it autodetect) and the view page highlights the snippet by file type.
- Copy all / Raw / Delete now on the view page.
- Accounts (optional) — email + password login (JWT in an httpOnly cookie). Sign in and your slips are tied to you and listed on a My Slips page. Owned slips can only be deleted by their owner.
- React + Vite frontend, Hono on Cloudflare Workers — one origin, one deploy.
Topics: cloudflare-workers · hono · react · vite · clipboard · pastebin · kv · encryption · self-hosted · typescript
┌────────────────────────── Cloudflare Worker ──────────────────────────┐
phone ──POST /api/slips──▶ Hono ──put──▶ KV (auto-expires via expirationTtl)
│ │ │
laptop ──GET /swift-otter──▶ SPA (React) ──GET /api/slips/swift-otter──▶ Hono ──get──▶ KV │
└───────────────────────────────────────────────────────────────────────┘
-
Frontend — a React SPA built by Vite. Client-side routes:
/(paste page) and/:code(view page, which fetches the slip from the API). -
Backend — a Hono app running on Cloudflare Workers. It serves the JSON API (
/api/slips,/api/slips/:code), auth (/api/auth/{register,login,logout,me}), the signed-in user's slips (/api/my/slips), the raw-text endpoint (/raw/:code), and a health check (/api/health). Every other path falls through to the SPA's static assets. -
Auth — email + password. Passwords are hashed with PBKDF2 (Web Crypto), and the session is a JWT (
hono/jwt) stored in an httpOnly cookie. Set aJWT_SECRET(see below). -
Storage — Workers KV, with prefixed keys so a slip lookup can never read a user record:
slip:<code>— the slip (carriesuserIdwhen created while signed in).user:<email>— the account record.userslip:<userId>:<code>— a per-user index entry (in KV metadata) sharing the slip's TTL, so My Slips is a singlekv.list()and expired entries drop off automatically.
KV's built-in
expirationTtldeletes expired slips for you, so there's no cleanup job.
The @cloudflare/vite-plugin
runs the Worker inside Vite's dev server, so the frontend and API share one
origin in development just like in production.
index.html # Vite entry — mounts the React app
src/
client/ # React SPA
main.tsx # router (/, /login, /my, /:code)
pages/ # Home (paste), View (read), Login, MySlips
components/ # shared Layout + nav
lib/api.ts # typed fetch wrappers
lib/auth.tsx # auth context (useAuth)
lib/crypto.ts # client-side AES-GCM encrypt/decrypt (passphrase slips)
lib/highlight.ts # highlight.js setup + language list
styles.css # the shared stylesheet
worker/ # Hono backend
index.ts # routes + SPA fallback
slips.ts # code generation, KV record shape, limits
auth.ts # password hashing + JWT sessions
shared/format.ts # pure helpers used by both client and worker
test/slips.test.ts # Vitest API tests (Hono app + mock KV)
wrangler.jsonc # Worker + KV + assets config
.dev.vars # local secrets (JWT_SECRET) — gitignored
You need a free Cloudflare account and Node.js installed. This repo uses pnpm.
# 1. Install deps
pnpm install
# 2. Provide a local auth secret (gitignored)
echo "JWT_SECRET=$(openssl rand -hex 32)" > .dev.vars
# 3. Run locally — Vite dev server + the Worker on one origin (local KV)
pnpm dev # http://localhost:5173
# 4. Run the tests (no account needed)
pnpm test
pnpm typecheck
# 5. Log in and create the KV namespace (first time only)
npx wrangler login
npx wrangler kv namespace create SLIP_KV
# -> copy the printed id into the kv_namespaces block in wrangler.jsonc
# 6. Set the production auth secret (stored encrypted by Cloudflare)
npx wrangler secret put JWT_SECRET
# 7. Build the SPA + Worker and ship it
pnpm deployWrangler prints your URL, e.g. https://slip.<your-subdomain>.workers.dev.
Bookmark it on both devices and you're done.
In the Cloudflare dashboard, add a route or custom domain to the Worker — e.g.
c.yourdomain.com — for even shorter links.
- Size cap: 200 KB per slip (plenty for logs; tune
MAX_BYTESinsrc/worker/slips.ts). - Privacy: friendly word-links are short and therefore guessable by someone enumerating combinations. For anything sensitive, use the Unguessable link toggle, which generates a long random code. By default slips are stored as plaintext in KV (not encrypted at rest), so short expiry is your friend. For true privacy, set a passphrase: the content is encrypted in the browser and only ciphertext reaches Cloudflare — but the passphrase is unrecoverable, so don't lose it. Encrypted slips skip the Raw endpoint and show a 🔒 (no preview) on My Slips.
- XSS: the view page renders slip content as React text nodes, so user input is escaped automatically.
- Free tier: Workers + KV free limits (100k reads/day, 1k writes/day) are far beyond personal use.
The original three ideas — QR handoff, client-side encryption, and syntax highlighting — are now built in (see the feature list above). Next up:
- Drag-and-drop / multi-file slips (still text, but tabbed).
- A self-destruct-on-first-read option (burn after reading).
- Web Share API integration so the result card can hand off to native share sheets.