A self-hosted pastebin/snippet sharing service. Web UI to paste and share a link, a CLI to pipe text straight in from the terminal. File-based storage by default, optional Postgres for multi-instance setups.
- Web UI — paste, set an optional expiry, optionally burn-after-read, get a shareable link
- Drag & drop a file straight into the paste box — language auto-fills from the file extension
- Language dropdown with auto-detect (via highlight.js) as the default, or pick one manually
- Live character/line counter as you type
- Syntax highlighting on the view page (via highlight.js), matched to the paste's language
- Dark/light theme toggle, remembered across visits, mobile-friendly layout throughout
- CLI client —
cat file.txt | node cli/pastebin.jsand get a URL back - Raw text endpoint for any paste (
/:id/raw) — good forcurl-ing into scripts - Expiry options: never, 10 minutes, 1 hour, 1 day, 7 days
- Burn-after-read — paste is deleted the moment it's viewed once
- Optional password protection — set a password at creation, viewers need it to read the paste (works with the web UI, the raw endpoint, and the JSON API)
- Early deletion — every paste gets a one-time delete token at creation, so you can remove it before it naturally expires (via the web UI's "Delete now" button, the CLI, or the API)
- File-based storage by default, one JSON file per paste — no database required to get started
- XSS-safe rendering (content is HTML-escaped before display, even with syntax highlighting layered on top)
git clone https://github.com/unitedevz/snippet-share.git
cd snippet-share
npm install
cp .env.example .env
npm startOpen http://localhost:3000. By default it runs with zero setup — file-based storage, no database needed.
Vercel needs two things this repo includes: vercel.json (rewrites every request to the serverless function, since paste routes like /:id aren't under /api/) and api/index.js (wraps the Express app as that function).
File storage works there with zero configuration — DATA_DIR automatically defaults to /tmp when it detects it's running on Vercel (only /tmp is writable on their platform; everywhere else is read-only). The tradeoff: /tmp isn't guaranteed to persist. It survives for the life of a warm container — often several minutes — but a cold start or redeploy can wipe it, so pastes can occasionally disappear unpredictably.
For anything you actually need to persist reliably, set STORAGE_DRIVER=postgres and DATABASE_URL in your Vercel project's environment variables — that's an upgrade you can make whenever it's convenient, not a requirement just to get the site working.
Set STORAGE_DRIVER in .env:
STORAGE_DRIVER |
Setup required | Notes |
|---|---|---|
file (default) |
None | One JSON file per paste in data/. Fine for personal use or a single instance. |
postgres |
DATABASE_URL |
Works with any managed Postgres — Neon, Supabase, Railway, RDS, or your own. Table is created automatically on first use. |
To use Postgres:
# .env
STORAGE_DRIVER=postgres
DATABASE_URL=postgres://user:password@host:5432/dbnameBoth backends implement the exact same interface, so nothing else about the app changes — same routes, same CLI, same behavior. Postgres is worth it once you're running more than one instance (e[...]
# from a file
node cli/pastebin.js notes.txt
# from stdin
cat error.log | node cli/pastebin.js
# with options
cat secret.txt | node cli/pastebin.js --expires 1h --burn --lang text
# pointing at a deployed instance instead of localhost
SERVER_URL=https://paste.yoursite.com node cli/pastebin.js notes.txt
# delete a paste early, using the token printed alongside its URL
node cli/pastebin.js delete <id> <deleteToken>
# password-protect a paste
cat secret.txt | node cli/pastebin.js --password hunter2Every create prints the URL to stdout and the delete command to stderr, so pastebin file.txt > urls.txt still captures just the URL — the delete token is right there in your terminal if you want it.
Fetching a password-protected paste programmatically (curl, scripts, etc.) needs the password in an X-Paste-Password header — never a query string, since those end up in server access logs and browser history:
curl -H "X-Paste-Password: hunter2" https://paste.yoursite.com/<id>/rawLink it globally with npm link to use it as a plain pastebin command.
docker build -t snippet-share .
docker run -p 3000:3000 -v $(pwd)/data:/app/data snippet-shareThe volume mount matters — without it, all pastes vanish when the container is removed.
npm testTwo layers of coverage:
tests/store.test.js— file backend against real file I/O in a temp directorytests/postgres-store.mock.test.js— postgres backend's query construction and logic against a mockedpgclient (no live database needed)tests/postgres-store.integration.test.js— the same postgres backend against a real Postgres. Skipped locally unlessTEST_DATABASE_URLis set; CI always runs it against a real Postgres[...]
server/store.js— dispatcher that picks the active backend based onSTORAGE_DRIVERserver/stores/file-store.js/server/stores/postgres-store.js— the two storage backends, same interfaceserver/render.js— HTML rendering with escaping (no XSS from pasted content)server/index.js— Express app: API routes (/api/pastes) and human-facing routes (/:id,/:id/raw)public/— the web UI (vanilla HTML/CSS/JS, no build step)cli/pastebin.js— standalone CLI client, talks to the same API
A background sweep runs every 10 minutes to remove expired pastes even if nobody visits them (both backends).
- Max paste size is 200,000 characters — adjust the limit in
server/store.jsif needed. - IDs are 8 hex characters (32 bits) — fine for a personal/small-team tool, not meant to resist a determined ID-guessing attacker at scale.
- No auth on paste creation by design (keeps the CLI simple). If you deploy this publicly, consider putting it behind a reverse proxy with rate limiting.
MIT — see LICENSE.