Status: MVP. Server + web app + Chrome extension work end-to-end on a single host. See PLAN.md for what's done and what's next.
Beam is an open-source, self-hostable replacement for PushBullet. Send URLs and files between your devices — phone, tablet, laptop, browser — using each platform's native Share affordance. End-to-end encrypted by default; the server is a dumb pipe that never sees your message contents.
Project codename / repo / CLI: routr. User-facing name: Beam.
- Server — Hono + SQLite, E2EE envelope routing, device-to-device WS delivery, signed-request REST auth, invite-based pairing, blob storage, device revocation, per-IP rate limiting, replay defense, expired- envelope cleanup, admin stats endpoint.
- Web app —
apps/web: setup, inbox (URL/file/note rendering), send (URL/file/note + multi-recipient), sent log, devices with fingerprints- revoke buttons, routing rules, settings. Keys in IndexedDB; live decryption in the browser.
- Chrome extension —
apps/extension-chrome: popup "Send this tab"- file send + recipient picker, Ctrl+Shift+B keyboard shortcut, context-menu items (link / tab / image), options page for routing rules + sent log, background WS with desktop notifications on incoming URLs/files/notes.
git clone https://github.com/akmad/routr.git
cd routr
docker compose up -dThe server listens on :3000. The first device to register becomes the
admin (no invite needed); after that, new devices require an invite from
an existing one.
To run without Docker (Node 22+):
pnpm install
pnpm --filter @routr/server startEnvironment variables (all optional):
| Var | Default | Purpose |
|---|---|---|
PORT |
8080 |
HTTP listen port |
HOST |
0.0.0.0 |
Bind address |
DATABASE_URL |
data/routr.db |
SQLite file path (or :memory:) |
BLOB_STORAGE_DIR |
data/blobs |
Where opaque blob files live |
LOG_LEVEL |
info |
pino log level |
Terminate TLS in front of Beam — it speaks plain HTTP/WS. Caddyfile:
beam.example.com {
reverse_proxy localhost:3000
}
Or with nginx, make sure to forward the Upgrade and Connection
headers so /api/v1/ws works:
location /api/v1/ws {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
}- Open
https://your-beam.example.com(the web app), or load the Chrome extension's popup. - Enter your server URL, leave the invite code blank — you're the first device, so you bootstrap the admin account.
- To pair another device: open Devices, click Generate invite, and paste that code into the new device's setup screen.
- Self-hostable server (single Docker container, SQLite by default)
- Web app — pair this browser as a device, send/receive, inbox
- Chrome extension — share the current tab via toolbar / right-click
- Firefox, Safari extensions
- Native desktop (macOS, Windows, Linux)
- iOS and Android apps
- Routing rules ("YouTube links go to Firefox")
- Cross-server peering (share with users on a different Beam server)
- E2E encrypted by default. The server only sees ciphertext, metadata, and routing info. It cannot read your messages.
- Self-host first. A single binary / Docker container, SQLite by default, zero external dependencies.
- Native share affordances. Beam plugs into the OS / browser share sheet instead of inventing its own. You share to Beam the way you already share to anything else.
- No accounts unless you want them. A Beam "account" is a set of devices that have cryptographically agreed to trust each other. Pairing a new device is a Syncthing-style code/QR exchange.
See SPEC.md §5.
AGPL-3.0. If you run a modified Beam server and let other people use it over a network, you have to share your modifications under the same license. See the LICENSE file for the full text.
This is a personal project and not currently soliciting contributions, but you are welcome to file issues and submit PRs. Discussion happens on GitHub Issues.