A shared clipboard for the devices on one network. Paste text or drop files on one device and they appear on every other device connected to the same Wi-Fi. No account, no app, nothing copied to the cloud, and everything clears itself after 30 minutes.
npm install
npm startLocalShare is running
this device http://localhost:3000
same Wi-Fi http://192.168.1.42:3000
items expire 30 min
Open the second address on your phone — or click Add a device and scan the QR code — and both devices see the same board. There is nothing to pair and nothing to type.
- Share text: paste, type,
Ctrl+Enterto send, one tap to copy on the other device - Share files: drag and drop, click to browse, or paste straight from the clipboard
- Image thumbnails, shrunk in the browser before upload so other devices load a few KB
- The board is written to disk, so a restart or a crash does not wipe it
- Everything appears on every device on the network in real time
- Filter the board by All / Text / Files
- Download every file at once as a zip
- Copy a link to any single item
- Delete one item, or clear the whole board
- Per-item countdown; items and their files are deleted when it runs out
- Light and dark themes
| Situation | Grouping |
|---|---|
| Self-hosted on a LAN | Same /24 subnet — 192.168.1.10 and 192.168.1.44 are together |
| Behind a reverse proxy | Same public address — one office or home NAT is one board |
The board is named by a hashed label (Onyx Valley), never by a raw address.
| Variable | Default | Meaning |
|---|---|---|
PORT |
3000 |
Listen port |
HOST |
0.0.0.0 |
Listen address |
ITEM_TTL_MINUTES |
30 |
How long an item survives |
MAX_FILE_MB |
512 |
Per-file size cap |
MAX_FILES_PER_UPLOAD |
20 |
Files accepted in one request |
MAX_ITEMS_PER_BOARD |
60 |
Board size; oldest entries fall off |
MAX_TEXT_CHARS |
20000 |
Per-snippet text cap |
MAX_BOARD_MB |
2048 |
Total bytes of files one board may hold |
WRITES_PER_MINUTE |
60 |
Write requests per address per minute |
PREVIEW_MAX_MB |
12 |
An image with no stored thumbnail is previewed from the original up to this size |
UPLOAD_DIR |
./uploads |
Where files are written |
DATA_DIR |
./data |
Where the board is saved so it survives a restart |
There is a copy of this table in .env.example with the reasoning next to each one.
| TRUST_PROXY | off | Set to 1 only behind a proxy you control |
TRUST_PROXY makes the server believe X-Forwarded-For. Without a proxy in front, anyone could set
that header and appear to be on another network's board, so leave it off when the process is exposed
directly.
LocalShare needs one long-lived process with a writable disk: the board is held in memory and mirrored to disk, a WebSocket pushes updates, and uploads have to land somewhere. Anything that gives you those two things will run it unchanged.
Two settings matter wherever you put it:
TRUST_PROXY=1whenever a proxy you control terminates TLS in front of the app (all the hosts below do). Without it every visitor looks like the proxy, so they all land on one board. With it set and no proxy in front, anyone can spoofX-Forwarded-For— so never turn it on for a directly exposed process.MAX_BOARD_MBshould sit comfortably under your volume size. It caps the total bytes of files one board holds; uploads past it are refused with a507rather than filling the disk.
git push # then: New > Blueprint, point it at this reporender.yaml sets the disk, the health check and TRUST_PROXY for you. Disks require
a paid instance; on the free plan delete the disk: block and the two *_DIR variables, and accept
that a restart empties the board.
fly launch --no-deploy # claim a name and region
fly volumes create localshare_data --size 1
fly deployfly.toml keeps one machine running, because a stopped machine drops every open
WebSocket.
Connect the repo — it detects Node and runs npm start. Add a volume mounted at /data, then set
TRUST_PROXY=1, UPLOAD_DIR=/data/uploads and DATA_DIR=/data/board.
docker compose up -ddocker-compose.yml builds the image and keeps /data in a named volume. Put
nginx or Caddy in front for TLS and set TRUST_PROXY=1. Without a proxy, leave it off.
No deployment at all: npm start on a laptop, a NAS or a Raspberry Pi, and use the LAN address it
prints. This is what the app is designed for — a public URL adds nothing, and it means your files
travel to someone else's server instead of staying on your own network.
Vercel, Netlify and Lambda cannot run this. Each request can hit a different instance with its own memory, so one device's board is invisible to another; there is no WebSocket, so nothing syncs; and the project directory is read-only.
vercel.json and api/index.js exist only so a Vercel deployment
boots instead of returning FUNCTION_INVOCATION_FAILED — writes go to /tmp and nothing binds a
port. The page loads and says in a banner that sharing will not work there. Treat it as a preview of
the interface, not a working install.
The app assumes a network you trust. Before putting it on the open internet:
- Everyone arriving from the same public address shares a board. Behind one home or office router that is the point. On mobile carrier NAT it can put strangers together.
- Put TLS in front of it. Traffic is plain HTTP otherwise, and the whole point is moving text and files around.
- Keep the limits tight.
MAX_BOARD_MB,MAX_FILE_MBandWRITES_PER_MINUTEare what stop one visitor filling your disk. The rate limit is per address, so it slows a script rather than stopping a determined one. - There is no authentication. Anyone who can reach the URL and shares your public address can read and clear that board. If you need it private, put it behind a VPN, an auth proxy, or basic auth in nginx.
- Everyone on the network shares one board by design. Do not put anything on it you would not hand to the person sitting next to you.
- Files are written to
UPLOAD_DIRunder a random opaque name; the original filename lives in the board file as metadata. They are deleted on expiry, on delete, or when the board is cleared. - The board now persists, so shared items and their files outlive a restart. They still expire on
their original schedule — persistence means a reboot is not the same as clearing the board, not that
items are kept forever. Raise
ITEM_TTL_MINUTESif you want a longer history, and deleteDATA_DIR/board.jsonto wipe it. - Files left over from a crash are swept at the next start — anything in
UPLOAD_DIRthe restored board does not reference is deleted, so giveUPLOAD_DIRa directory of its own. - Traffic is plain HTTP. Fine on a home or office LAN; put it behind a reverse proxy with TLS if it is reachable from anywhere else.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/session |
Board label, connected devices, items, limits, join URL, QR |
POST |
/api/text |
Share a text snippet |
POST |
/api/files |
Multipart upload, field name files |
GET |
/api/items/:id/download |
Download one item |
GET |
/api/items/:id/preview |
Inline image preview: the stored thumbnail, or a small original |
GET |
/api/archive |
Every file on the board as a zip |
DELETE |
/api/items/:id |
Remove one item |
POST |
/api/clear |
Empty the board, or one kind ({"kind":"text"|"file"}) |
GET |
/s/:id |
Shareable link — text page, or redirect to the file |
WS |
/ws |
Live board state |
server.js express app, routes, uploads, previews, zip, WebSocket
src/config.js environment-driven settings
src/store.js the board, disk persistence, expiry, file cleanup
src/net.js address normalisation, subnet grouping, board labels
public/ index.html, styles.css, app.js
api/index.js serverless entry point (boots only — see Deploying)