Skip to content

Repository files navigation

LocalShare

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.

Run it

npm install
npm start
LocalShare 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.

What it does

  • Share text: paste, type, Ctrl + Enter to 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

How devices are grouped

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.

Configuration

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.

Deploying

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=1 whenever 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 spoof X-Forwarded-For — so never turn it on for a directly exposed process.
  • MAX_BOARD_MB should sit comfortably under your volume size. It caps the total bytes of files one board holds; uploads past it are refused with a 507 rather than filling the disk.

Render

git push          # then: New > Blueprint, point it at this repo

render.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.io

fly launch --no-deploy                        # claim a name and region
fly volumes create localshare_data --size 1
fly deploy

fly.toml keeps one machine running, because a stopped machine drops every open WebSocket.

Railway

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, or your own VPS

docker compose up -d

docker-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.

On your own network

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.

Serverless will not work

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.

Going public

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_MB and WRITES_PER_MINUTE are 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.

Security notes

  • 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_DIR under 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_MINUTES if you want a longer history, and delete DATA_DIR/board.json to wipe it.
  • Files left over from a crash are swept at the next start — anything in UPLOAD_DIR the restored board does not reference is deleted, so give UPLOAD_DIR a 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.

API

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

Layout

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)

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages