Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RemoteLink
A self-hosted remote support platform similar to ScreenConnect / ConnectWise Control. Connect to and control remote machines through a browser — no port forwarding required.
Features
- Browser-based viewer — full mouse, keyboard, and scroll control over WebSocket; no plugins
- Multi-monitor — switch between displays mid-session
- Clipboard sync — paste from your clipboard to the remote machine and vice versa
- File transfer — drag-and-drop upload, directory browser, one-click download from remote
- Script execution — run PowerShell, cmd, or bash commands and stream output back (admin only)
- Session chat — text chat forwarded to the remote machine during a session
- Wake-on-LAN — send a magic packet to wake an offline machine
- Attended mode — agent prompts the local user to accept before the session starts
- Agent auto-update — agents check their version on each heartbeat and self-replace when a new binary is published
- Session recording — relay can archive JPEG frame streams to disk (opt-in)
- Machine tags & notes — label and annotate machines; filter by tag or group
- Machine groups — organise machines and control technician access
- Invite-only registration — no open sign-up; admin sends email invites
- Open enrollment — agents register without a token when
OPEN_ENROLLMENT=true - Light / dark / system theme
- Mass deployment — NSIS silent installer for Windows; shell one-liner for Linux
Architecture
Browser ──── HTTPS ────▶ Next.js App (port 3000)
│ │
│ PostgreSQL 17
│
Browser ──── WSS ─────▶ Relay (port 8765)
│
Agent ──── WSS ─────▶ Relay ─────┘
| Component | Technology | Purpose |
|---|---|---|
| Web app | Next.js 16 / NextAuth v5 / Drizzle ORM | Dashboard, auth, API |
| Relay | FastAPI + asyncpg (Python) | WebSocket bridge between agent and browser |
| Database | PostgreSQL 17 | Machines, sessions, tokens, groups |
| Agent | Python + PyInstaller | Screen capture, input control, file transfer |
Quick Start (Docker)
1. Clone and configure
git clone https://git.pathcore.org/monoadmin/remotelink-docker.git
cd remotelink-docker
cp .env.example .env
Edit .env:
POSTGRES_PASSWORD=a_strong_random_password
AUTH_SECRET= # generate: openssl rand -base64 32
NEXT_PUBLIC_APP_URL=http://your-server-ip:3000
NEXT_PUBLIC_RELAY_URL=your-server-ip:8765
2. Start the stack
docker compose up -d
Three containers start:
remotelink-app— web dashboard on port 3000remotelink-relay— WebSocket relay on port 8765remotelink-db— PostgreSQL (internal only)
3. Create your first account
Open http://your-server:3000 and sign up. The first account is automatically given the admin role.
After the first account is created, registration is invite-only by default. Additional users require an invite link from Admin → User Invites.
Production Deployment (Reverse Proxy)
Nginx example
# Web app
server {
listen 443 ssl;
server_name remotelink.example.com;
ssl_certificate /etc/letsencrypt/live/remotelink.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/remotelink.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# WebSocket relay
server {
listen 443 ssl;
server_name relay.remotelink.example.com;
ssl_certificate /etc/letsencrypt/live/remotelink.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/remotelink.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8765;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
}
Update .env for production:
NEXT_PUBLIC_APP_URL=https://remotelink.example.com
NEXT_PUBLIC_RELAY_URL=relay.remotelink.example.com # no port — nginx handles 443
ALLOWED_ORIGINS=https://remotelink.example.com
Rebuild the app container after changing NEXT_PUBLIC_* variables (they are baked into the Next.js build):
docker compose up -d --build app
Environment Variables
App (docker-compose.yml → app service)
| Variable | Default | Description |
|---|---|---|
POSTGRES_PASSWORD |
— | Required. PostgreSQL password |
AUTH_SECRET |
— | Required. NextAuth signing secret (openssl rand -base64 32) |
NEXT_PUBLIC_APP_URL |
http://localhost:3000 |
Public URL shown in invite links |
NEXT_PUBLIC_RELAY_URL |
localhost:8765 |
Relay address as seen from the browser (host:port or host if behind proxy on 443) |
OPEN_ENROLLMENT |
false |
Set true to allow agents to register without a token |
OPEN_ENROLLMENT_USER_EMAIL |
— | Email of the user who owns auto-registered machines. Falls back to first admin. |
Relay (docker-compose.yml → relay service)
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
— | Required. PostgreSQL connection string |
ALLOWED_ORIGINS |
* |
Comma-separated allowed CORS origins. Set to your app URL in production. |
RECORDING_DIR |
— | Path inside the container to save session recordings. Leave empty to disable. |
Agent Installation
Linux (one-liner)
# With enrollment token
curl -L https://your-server.com/downloads/remotelink-agent-linux -o remotelink-agent
chmod +x remotelink-agent
./remotelink-agent --server https://your-server.com --enroll YOUR_TOKEN
# Open enrollment (no token)
./remotelink-agent --server https://your-server.com
Config is saved to /etc/remotelink/agent.json. On subsequent runs, just:
./remotelink-agent
Systemd service (run on boot)
# /etc/systemd/system/remotelink-agent.service
[Unit]
Description=RemoteLink Agent
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/remotelink-agent
Restart=always
RestartSec=10
User=root
[Install]
WantedBy=multi-user.target
cp remotelink-agent /usr/local/bin/
systemctl daemon-reload
systemctl enable --now remotelink-agent
Windows (NSIS installer)
Download RemoteLink-Setup.exe from your dashboard (Admin → Agent Enrollment) or build it yourself (see Building the Windows Installer).
Interactive install — double-click and follow the wizard. Enter your server URL and optional enrollment token when prompted.
Silent mass deployment — no UI, suitable for GPO / RMM / Intune:
:: Open enrollment (no token required)
RemoteLink-Setup.exe /S /SERVER=https://your-server.com
:: With enrollment token
RemoteLink-Setup.exe /S /SERVER=https://your-server.com /ENROLL=YOUR_TOKEN
The installer:
- Copies binaries to
C:\Program Files\RemoteLink\ - Enrolls the machine with the server
- Installs and starts a Windows service (
RemoteLinkAgent) that runs on boot
Uninstall:
"C:\Program Files\RemoteLink\Uninstall.exe" /S
macOS
curl -L https://your-server.com/downloads/remotelink-agent-macos -o remotelink-agent
chmod +x remotelink-agent
./remotelink-agent --server https://your-server.com --enroll YOUR_TOKEN
macOS binaries require the agent to be built on a Mac (PyInstaller limitation). See Building the Agent.
Agent CLI Reference
remotelink-agent [OPTIONS]
Options:
--server URL Server URL (required for first-time registration)
--relay URL Relay WebSocket URL (auto-derived from --server if omitted)
--enroll TOKEN Enrollment token (omit if open enrollment is enabled)
--attended Prompt local user to accept each incoming connection
--run-once Exit when the session ends (don't save config)
--verbose, -v Verbose logging
Agent Enrollment
Option A — Enrollment Tokens (default)
- Go to Admin → Agent Enrollment
- Create a token (optional: label, expiry, max uses)
- Copy the install command shown in the UI and run it on the target machine
Tokens can be single-use (max uses = 1) for one machine, or unlimited for mass deployment.
Option B — Open Enrollment
Set OPEN_ENROLLMENT=true in .env and restart the app container. Agents can then register without any token:
./remotelink-agent --server https://your-server.com
Machines appear immediately in the dashboard under the account set by OPEN_ENROLLMENT_USER_EMAIL (or the first admin user).
Security note: Open enrollment allows any machine that can reach your server to register. Only enable it on private/VPN-accessible deployments or temporarily during a rollout.
Connecting to a Machine
Session code (from the agent's tray / terminal)
- The agent generates a 6-character session code on demand
- Go to Quick Connect in the dashboard and enter the code
- The viewer opens directly — no relay polling required
Direct connect (from Machines page)
Click Connect next to any online machine. A session is created and the viewer opens.
Viewer Controls
| Action | How |
|---|---|
| Mouse | Move / click / right-click / scroll normally on the canvas |
| Keyboard | Type directly — all keystrokes are forwarded |
| Ctrl+Alt+Del | Toolbar → CAD button |
| Paste clipboard | Toolbar → clipboard icon (reads your local clipboard and sends it to the remote) |
| Switch monitor | Toolbar → monitor dropdown (appears when agent has >1 display) |
| File transfer | Toolbar → folder icon → sidebar opens |
| Chat | Toolbar → message icon → sidebar opens |
| Quality | Toolbar → signal icon → High / Medium / Low |
| Fullscreen | Toolbar → maximise icon (toolbar auto-hides after 3 s, reappears on mouse move) |
| Wake-on-LAN | Toolbar → lightning bolt (shown when agent is offline and MAC address is known) |
| End session | Toolbar → X button |
File Transfer
The file transfer sidebar has two areas:
Remote file browser (top half)
- Navigate directories on the remote machine
- Click a file's download button to pull it to your browser
- The agent's home directory (
~) is the default starting location
Upload drop zone (bottom half)
- Drag files from your computer onto the drop zone, or click to browse
- Files are sent to the remote machine's current directory (shown in the browser path)
Files are transferred as base64-encoded chunks through the existing WebSocket relay — no separate HTTP upload server needed.
Machine Management
Tags
Add colour-coded tags to machines (e.g. server, windows, client-acme). Tags appear as badges on each machine card and can be used to filter the machines list.
- Click the three-dot menu on a machine card → Edit
- Type a tag in the tag input and press Enter
- Click the × on a tag badge to remove it
Built-in tag colours: server (blue), windows (cyan), linux (orange), mac (grey), workstation (purple), laptop (green). Unknown tags use the default muted style.
Notes
Free-text notes per machine — useful for recording IP ranges, owner contacts, software installed, etc.
- Click the three-dot menu → Show notes to view inline
- Click Edit to modify
Groups
Groups let you organise machines by client, location, or team.
- Go to Admin → Groups and create a group
- Edit a machine card and select a group from the dropdown
- Use the group filter on the Machines page to view only machines in a group
Session Recording
To enable recording, set RECORDING_DIR in the relay service environment:
# docker-compose.yml
relay:
environment:
RECORDING_DIR: /recordings
volumes:
- ./recordings:/recordings
Each session produces a .remrec file in that directory. The format is:
Header: "RLREC\x01" (6 bytes)
Frames: [8-byte ms timestamp][4-byte length][JPEG bytes] × N
A simple Python replay script:
import struct, time
with open("session.remrec", "rb") as f:
assert f.read(6) == b"RLREC\x01"
while True:
hdr = f.read(12)
if not hdr: break
ts, length = struct.unpack(">QI", hdr)
frame = f.read(length)
# frame is a JPEG — display with Pillow, cv2, etc.
Building the Agent
Linux / macOS
cd agent
pip install pyinstaller -r requirements.txt
pyinstaller agent.spec
# Output: dist/remotelink-agent
The Linux binary requires xclip or xsel on the target machine for clipboard sync.
Windows
Building must be done on a Windows machine — PyInstaller cannot cross-compile Windows executables from Linux.
Prerequisites:
- Python 3.11+ from python.org
- NSIS 3.x (for the installer)
cd agent
pip install pyinstaller
pip install -r requirements.txt
pyinstaller agent.spec
# Output: dist\remotelink-agent.exe + dist\remotelink-agent-service.exe
makensis installer.nsi
# Output: RemoteLink-Setup.exe
Note: agent.spec references assets\icon.ico. Either add your own .ico to agent\assets\ or remove the icon= and !define MUI_ICON lines from agent.spec and installer.nsi respectively to use the defaults.
Security
Authentication
- Invite-only user registration — no public sign-up
- Passwords hashed with bcrypt (via NextAuth credentials provider)
- JWT sessions with configurable secret (
AUTH_SECRET)
Agent authentication
- Each machine has a unique 256-bit
access_keygenerated at enrollment - The relay validates the access key against the database before accepting any agent connection
- Viewer connections are authenticated via a per-session
viewer_token(UUID)
Script execution
exec_scriptcommands from the viewer are only forwarded to the agent if the viewer's user has theadminrole- Checked at the relay level — non-admin viewers receive an error; the command never reaches the agent
Transport
- All connections should be run behind TLS in production (nginx + Let's Encrypt)
- The relay enforces
ALLOWED_ORIGINSCORS to prevent cross-site WebSocket hijacking
Attended mode
- Running the agent with
--attendedmakes it prompt the local user (via zenity / kdialog on Linux, PowerShell MessageBox on Windows) before any session starts - The connection is rejected if the user declines or doesn't respond within 30 seconds
Development
Prerequisites
- Node.js 22+ with pnpm (
npm i -g pnpm) - Docker + Docker Compose
- Python 3.12+
Run locally
# Start database only
docker compose up -d postgres
# Install JS dependencies
pnpm install
# Set up .env (copy from .env.example, fill in values)
cp .env.example .env
# Start the Next.js dev server
pnpm dev
# In another terminal — start the relay
cd relay
pip install -r requirements.txt
DATABASE_URL=postgresql://remotelink:password@localhost:5432/remotelink uvicorn main:app --port 8765 --reload
# In another terminal — run the agent
cd agent
pip install -r requirements.txt
python agent.py --server http://localhost:3000 --enroll YOUR_TOKEN
Database schema
| Table | Purpose |
|---|---|
users |
User accounts with roles (admin, user) |
machines |
Registered agent machines (online status, tags, notes, group) |
groups |
Machine groups for access control |
sessions |
Active and historical viewer sessions |
session_codes |
Short-lived 6-character codes for Quick Connect |
enrollment_tokens |
Admin-generated tokens for agent registration |
invites |
Email invite tokens for new user sign-up |
Migrations are in db/migrations/ and applied automatically when the PostgreSQL container initialises from scratch.
Project structure
remotelink-docker/
├── app/ # Next.js App Router pages and API routes
│ ├── (dashboard)/ # Authenticated dashboard pages
│ │ └── dashboard/
│ │ ├── admin/ # Admin panel (invites, enrollment, groups)
│ │ ├── connect/ # Quick Connect (session code entry)
│ │ ├── machines/ # Machine list with search/filter
│ │ ├── sessions/ # Session history
│ │ └── settings/ # User profile settings
│ ├── api/ # REST API handlers
│ │ ├── agent/ # Agent: register, heartbeat, session-code
│ │ ├── groups/ # Machine groups CRUD
│ │ ├── machines/ # Machine CRUD + Wake-on-LAN
│ │ ├── sessions/ # Session read/update
│ │ └── ...
│ └── viewer/[sessionId]/ # Full-screen remote viewer
├── agent/ # Python agent
│ ├── agent.py # Main agent (capture, input, file transfer)
│ ├── service_win.py # Windows Service wrapper
│ ├── agent.spec # PyInstaller build spec
│ ├── installer.nsi # NSIS Windows installer script
│ └── requirements.txt
├── components/
│ ├── dashboard/ # Dashboard UI components
│ │ ├── machine-card.tsx # Machine card with inline edit
│ │ └── machines-filter.tsx # Search/tag/group filter bar
│ └── viewer/ # Viewer UI components
│ ├── toolbar.tsx # Session toolbar
│ ├── connection-status.tsx
│ ├── file-transfer-panel.tsx
│ └── chat-panel.tsx
├── db/migrations/ # SQL migration files
├── lib/db/schema.ts # Drizzle ORM schema
├── relay/
│ ├── main.py # FastAPI WebSocket relay
│ ├── Dockerfile
│ └── requirements.txt
├── docker-compose.yml
├── Dockerfile # Multi-stage Next.js production build
└── .env.example
Relay WebSocket Protocol
Agent → Relay (/ws/agent?machine_id=&access_key=)
| Message | Direction | Type | Description |
|---|---|---|---|
| JPEG frame | agent→relay | binary | Raw screen frame forwarded to all viewers |
{"type":"ping"} |
agent→relay | JSON | Updates last_seen; relay responds with online status |
{"type":"monitor_list","monitors":[...]} |
agent→relay | JSON | Sent after start_stream; relayed to viewer |
{"type":"clipboard_content","content":"..."} |
agent→relay | JSON | Clipboard change; relayed to viewer |
{"type":"file_chunk",...} |
agent→relay | JSON | File download chunk; relayed to viewer |
{"type":"file_list","entries":[...]} |
agent→relay | JSON | Directory listing; relayed to viewer |
{"type":"script_output",...} |
agent→relay | JSON | Script stdout line; relayed to viewer |
{"type":"chat_message","text":"..."} |
agent→relay | JSON | Chat message; relayed to viewer |
Viewer → Relay (/ws/viewer?session_id=&viewer_token=)
| Message | Direction | Type | Description |
|---|---|---|---|
{"type":"start_stream"} |
relay→agent | JSON | Sent by relay when viewer connects |
{"type":"stop_stream"} |
relay→agent | JSON | Sent by relay when viewer disconnects |
{"type":"mouse_move","x":...,"y":...} |
viewer→agent | JSON | |
{"type":"mouse_click","button":"left",...} |
viewer→agent | JSON | |
{"type":"mouse_scroll","dx":...,"dy":...} |
viewer→agent | JSON | |
{"type":"key_press","key":"a"} |
viewer→agent | JSON | Printable character |
{"type":"key_special","key":"enter"} |
viewer→agent | JSON | Special key |
{"type":"exec_key_combo","keys":["ctrl_l","alt_l","delete"]} |
viewer→agent | JSON | Key combo (admin only) |
{"type":"set_monitor","index":1} |
viewer→agent | JSON | Switch capture monitor |
{"type":"set_quality","quality":"high"} |
viewer→agent | JSON | high / medium / low |
{"type":"clipboard_paste","content":"..."} |
viewer→agent | JSON | Set remote clipboard |
{"type":"exec_script","script":"...","shell":"bash"} |
viewer→agent | JSON | Run script (admin only) |
{"type":"file_download","path":"/tmp/log.txt"} |
viewer→agent | JSON | Request file |
{"type":"file_upload_start","filename":"...","dest_path":"..."} |
viewer→agent | JSON | |
{"type":"file_upload_chunk","chunk":"<base64>","seq":0} |
viewer→agent | JSON | |
{"type":"file_upload_end"} |
viewer→agent | JSON | |
{"type":"list_files","path":"~"} |
viewer→agent | JSON | Browse remote filesystem |
{"type":"chat_message","text":"..."} |
viewer→agent | JSON |
Troubleshooting
Agent connects but viewer shows blank screen (Linux)
The agent can't find a display. Check:
echo $DISPLAY # should be :0 or similar
xdpyinfo # should list display info
The agent auto-detects $DISPLAY by scanning /tmp/.X11-unix/. If you're running the agent as a system service, make sure DISPLAY is set in the service environment.
Relay shows "Cannot call receive once a disconnect message has been received"
This was a known bug — it is fixed. If you see it on an older version, update the relay container:
docker compose up -d --build relay
File transfer fails for large files
The relay holds chunks in memory. For files over ~100 MB, consider increasing the Docker memory limit or using an alternative file transfer method for large transfers. Chunk size is 64 KB.
Agent auto-update isn't working
The relay's CURRENT_AGENT_VERSION in app/api/agent/heartbeat/route.ts must be bumped and the new binary placed at /public/downloads/remotelink-agent-linux (and /windows.exe). The version check is a simple string equality — it won't auto-update unless the constant is changed.
Wake-on-LAN doesn't work
- The agent must have connected at least once since the MAC address feature was added (it's reported on registration)
- WoL only works if the target machine and the server are on the same L2 network segment, or your router forwards broadcast packets
- The machine's BIOS must have WoL enabled, and the NIC must be configured to wake on magic packet
Licence
MIT