---
## Why
Moving a URL, a snippet, or a file from your laptop to your phone is still solved badly. AirDrop is Apple-only. Nearby Share is Android-only. Everything genuinely cross-platform means emailing yourself, DMing yourself on Slack, or uploading to someone else's cloud.
CLIPLINK is the browser-native answer. Open a page, get a short room code, share it with your other device — anything you send appears there instantly and is copied to the clipboard automatically.
Nothing persists. Rooms expire on a timer, history lasts only as long as the room, and files never touch a server at all.
## Features
- **Zero auth, zero install** — create or join a room in one click, in any modern browser
- **Join three ways** — room code (random, or one you choose, with an optional passphrase), shared URL (`/room/X7KP2M#k=…`), or in-room QR code, encoded locally so the key never leaves the device
- **Encrypted both ways** — private rooms generate an AES-GCM-256 key in the browser that never reaches the server, carried in the URL fragment or pasted in; open rooms derive their key from the code, so the code alone opens them. Private rooms joined without a key open locked and unlock in place
- **Realtime text sync** — WebSocket transport, with HTTP polling as an automatic fallback
- **Auto-copy on receive** — incoming clips land on your clipboard, with a toast and a subtle flash
- **Nothing missed in a background tab** — a browser will not let a hidden tab write the clipboard, so a clip that arrives there is counted in the tab title and on the installed app's icon, and copied when you come back — or, where the browser wants a click first, offered with a Copy button. System notifications are opt-in, and say which room, never what the clip holds. They come from the open tab rather than a push server, so they last as long as the browser keeps the tab alive
- **History that survives a reload** — the room's last 50 clips with direction, the sending device's name, timestamp, one-click copy, expand-in-place, and an Open action on link clips. Device names are sealed with the room key, so the server never sees them; rename yours from the devices sheet
- **Delete a clip, or clear the lot** — for every device at once, and only by someone holding the room key: the room code alone deletes nothing
- **One-time clips** — for passwords and codes. Never drawn in the history list, copied on arrival, and deleted for everyone by the first device that reads them
- **Keyboard-first** — every room action has a binding; `?` shows the cheat sheet and `⌘K`/`Ctrl+K` opens the command palette
- **Know the state** — who else is in the room, a live expiry countdown, and a badge that says when you have dropped to the polling fallback
- **Peer-to-peer file transfer** — up to 500 MB per file over WebRTC data channels, via attach, drag-and-drop, or paste. Drop or attach a whole folder (up to 50 files) and it arrives as one group, with Download all saving it into a folder of your choice where the browser allows it, or as a single zip (up to 1 GB, held in memory until it's saved). Bytes flow browser-to-browser; the server only relays signaling. Every block is SHA-256 verified and the whole file is checked against a digest that travelled over signaling rather than the data channel. A download can be paused and picked back up, survives a dropped connection, and now survives a reload as well — a refreshed tab resumes from what is already on disk. Large files (64 MB+) stream straight to disk in every current browser, and progress shows speed and time left. The transfer engine is published on its own as [`@thebkht/rtc-file-transfer`](https://www.npmjs.com/package/@thebkht/rtc-file-transfer)
- **Share sheet target** — install CLIPLINK as an app and it appears in the OS share sheet. Shared text lands in the editor of a room open in another tab, or in one you create or join; shared files are offered to the room. The service worker keeps them on the device, so they never reach the server. Works on Android Chrome and installed desktop Chromium; iOS has no share targets for web apps
- **A CLI** — `git log -1 | cliplink send` creates a room, sends it, and prints a QR code for the phone. `cliplink recv --one | pbcopy` waits for the next clip and copies it, and `cliplink send --burn` sends a one-time clip. Same rooms, same encryption; `npm i -g @thebkht/cliplink`, documented in [CLI.md](packages/cliplink/CLI.md)
- **Ephemeral by design** — per-room TTL configurable from 1h to 24h (6h default), max 50 clips retained per room
- **Rate limited** — token buckets shared across instances through Redis: a burst of 60 clips refilling at 1/second, and 10 room creations refilling at 1 per 10 seconds
Exact limits live in [`lib/cliplink/constants.ts`](lib/cliplink/constants.ts).
### Keyboard shortcuts
`⌘` is `Ctrl` on Windows and Linux.
| Keys | Action |
| --- | --- |
| `Enter` | Send the clip |
| `⇧Enter` / `⌘Enter` | New line instead of sending |
| `⌘K` | Command palette |
| `⌘⇧C` | Copy the latest received clip |
| `⌘⇧V` | Paste from device into the editor |
| `⌘⇧⌫` | Clear the editor (undoable) |
| `?` | This list |
These work anywhere. The rest need focus to be outside the compose box:
| Keys | Action |
| --- | --- |
| `E` or `/` | Focus the editor |
| `L` | Copy the room link |
| `Q` | Show the QR code |
| `D` | Show the devices in the room, and rename this one |
| `A` | Attach files |
| `F` | Attach a folder |
| `T` | Toggle the theme |
| `N` | Turn notifications on or off |
| `X` | Leave the room |
| `1`–`9` | Copy that history row |
| `Esc` | Close a sheet, cancel a pending Leave or Clear All, or leave the editor |
Typing any other character with nothing focused starts a clip.
## Architecture
| Layer | Implementation |
| --- | --- |
| Framework | Next.js 16 App Router, React 19, TypeScript |
| Styling | Tailwind CSS v4 |
| Storage | Upstash Redis (hash + sorted set per room), in-memory fallback |
| Realtime | WebSocket primary, HTTP polling fallback |
| Fan-out | Redis pub/sub via `ioredis`, process-local `EventEmitter` fallback |
| Files | WebRTC data channels, peer-to-peer |
| Hosting | Vercel — stock Next.js build, no custom adapter |
Two things here are worth reading even if you never run CLIPLINK:
**The transport abstraction.** Both realtime transports implement one `TransportClient` interface ([`packages/cliplink/src/types.ts`](packages/cliplink/src/types.ts)), so the room UI's retry/backoff/fallback state machine is written once. WebSockets ([`ws.ts`](packages/cliplink/src/ws.ts)) replaced an earlier SSE implementation without the UI changing at all; polling ([`http.ts`](packages/cliplink/src/http.ts)) remains the fallback. The same interface is what let a CLI reuse the whole stack: it supplies `ws` in place of the browser's global and gets the same encryption, the same backoff and the same fallback. Running WebSockets on Vercel Functions is documented thinly elsewhere — this is a complete working example.
**One implementation of the encryption.** The crypto, wire types and transports live in [`@thebkht/cliplink`](packages/cliplink), consumed by the app through thin re-exports in `lib/cliplink/`. A second implementation is the expensive part of every non-browser surface and the one most likely to drift, so there is exactly one — and its tests pin ciphertexts from earlier builds, since tabs opened before a deploy keep talking to ones opened after it.
**Server-free file transfer.** [`@thebkht/rtc-file-transfer`](packages/rtc-file-transfer) implements offer/accept, chunking with backpressure in both directions, stall detection, block-level integrity checks and a whole-file digest, pause, resume — across a dropped connection or a reload — streaming to disk, and withdrawal over raw WebRTC data channels, with no TURN-dependent SaaS in the middle. It is stable at 1.x: the API follows semver and wire protocol v1 is permanent, so any two versions interoperate. It lives in this repo as a workspace package, is [published to npm](https://www.npmjs.com/package/@thebkht/rtc-file-transfer) with provenance, and works with any signaling channel — with adapters ready for WebSocket, `BroadcastChannel`, Supabase Realtime, Trystero, PeerJS and simple-peer:
```bash
npm install @thebkht/rtc-file-transfer
```
The API surface is four routes: `POST /rooms` (create), `GET /rooms/:code` (fetch), `POST|GET|DELETE /rooms/:code/clips` (send / poll after id / delete, the last authorised by a token derived from the room key), and `GET /rooms/:code/socket` (WebSocket upgrade for clips and signaling).
## Getting started
**No Redis and no Vercel account are required.** Without credentials the app falls back to an in-memory store and a process-local event bus, which is enough to develop against and to sync between two tabs on one machine.
```bash
git clone https://github.com/thebkht/cliplink.git
cd cliplink
pnpm install
pnpm dev
```
Open