Browser-based spatial workspace for developers: infinite canvas, draggable SSH terminals, remote browser windows, and project context, all in one place.
Infinite is a browser-based spatial workspace for development tools. It gives you an infinite canvas with draggable windows for SSH sessions, notes, a remote browser, and project context.
- Infinite canvas with pan/zoom and persistent window layouts
- SSH connections inside draggable xterm.js windows
- Optional SSH relay agent for private networks, Tailscale, or LAN-only hosts
- Remote browser windows backed by Puppeteer
- Notes and project-oriented workspace state stored in PostgreSQL
- Next.js 16
- React 19
- Tailwind CSS v4
- Express + WebSocket
- Prisma + PostgreSQL
- xterm.js
- ssh2
- Puppeteer
The fastest way to try Infinite. Runs the database, frontend, and relay server in one command:
docker compose up -d --buildOpen: http://localhost:7890
Ports:
- frontend:
http://localhost:7890 - relay server:
http://localhost:7891 - database:
localhost:5432(infinite/infinite/infinite)
The frontend container runs prisma db push on startup, so the database
schema is created automatically. No .env file is required for the Docker
setup.
Stop the stack:
docker compose downWipe the database volume too:
docker compose down -vUse this path if you want to run the app directly with Node.js instead of Docker.
- Node.js 20+
- PostgreSQL 14+ running on
localhost:5432 - npm
Note:
npm installdownloads Puppeteer's bundled Chromium (~300 MB). If you're on Alpine, behind a strict proxy, or low on disk, see Puppeteer troubleshooting below.
cp .env.example .envGenerate a 64-character hex secret for encrypting saved SSH credentials:
openssl rand -hex 32Put the output in .env as ENCRYPTION_SECRET. A complete .env looks
like:
DATABASE_URL=postgresql://infinite:infinite@localhost:5432/infinite
DIRECT_URL=postgresql://infinite:infinite@localhost:5432/infinite
ENCRYPTION_SECRET=<paste-your-generated-secret-here>
NEXT_PUBLIC_WS_URL=
ALLOWED_ORIGINS=http://localhost:3000Variable reference:
DATABASE_URL: main Prisma/app database connectionDIRECT_URL: direct database connection for Prisma operationsENCRYPTION_SECRET: encrypts saved SSH passwords and private keys — if you lose this, saved credentials cannot be recoveredNEXT_PUBLIC_WS_URL: leave empty for local dev; set to your relay server URL when the frontend and WS server run on different originsALLOWED_ORIGINS: origins allowed to call the Express/WebSocket server
The default .env.example expects a PostgreSQL role infinite with password
infinite and a database named infinite. Pick one:
Option A — run Postgres in Docker (easiest if you don't have Postgres installed):
docker run -d --name infinite-pg \
-e POSTGRES_USER=infinite \
-e POSTGRES_PASSWORD=infinite \
-e POSTGRES_DB=infinite \
-p 5432:5432 \
postgres:16-alpineOption B — use a native Postgres install:
# macOS (Homebrew)
brew services start postgresql@16
createuser infinite -P # enter "infinite" as the password when prompted
createdb infinite -O infinite
# Debian / Ubuntu
sudo -u postgres psql -c "CREATE USER infinite WITH PASSWORD 'infinite';"
sudo -u postgres psql -c "CREATE DATABASE infinite OWNER infinite;"npm install
npm run db:pushnpm install also runs patch-package and prisma generate via
postinstall, so you don't need to run them manually.
npm run devThat starts the Next.js frontend on http://localhost:3000 and the
Express/WebSocket relay server on http://localhost:7891. Open
http://localhost:3000.
npm run build
npm run lintMain models in prisma/schema.prisma:
Connection: saved SSH targets and encrypted credentialsLayout: saved canvas/window stateAgent: relay agents for private-network SSHProject: project workspace stateNote: notesBookmark: saved URLs
For local development this app uses a fixed local user id, so no auth setup is currently required.
In the SSH panel:
- Click
Add Connection - Fill in:
- name
- host
- port
- username
- auth method: password or private key
- Leave the route as
Via Fly server (public IP)if the target is publicly reachable from the relay server - Save and click
Connect
This opens an SSH terminal window on the canvas.
If a saved connection supports it, use the Dev button beside that SSH
connection to open the browser window attached to the same backend
connection.
The agent exists for SSH targets that are not publicly reachable.
Examples:
- a machine on your home LAN
- a private cloud VM
- a host only reachable through Tailscale or another VPN
Without an agent:
- the server connects directly to
host:port
With an agent:
- a small Node.js process runs on a machine that can reach the private host
- that process connects back to Infinite over WebSocket
- Infinite tells that process to open the SSH session on its behalf
Use an agent when the SSH target is reachable from your machine or private network, but not reachable from the public relay server.
In the Agent panel:
- Click
Create Agent - Copy the generated command
- Run that command on the machine that has network access to the target host
The command looks like:
INFINITE_TOKEN=... INFINITE_SERVER=ws://localhost:7891 node agent/index.jsWhen connected successfully, the agent will show as online.
You can also run it yourself from this repo:
cd agent
npm install
INFINITE_TOKEN=your-token INFINITE_SERVER=ws://localhost:7891 node index.jsRequired environment variables:
INFINITE_TOKEN: generated by the app when you create the agentINFINITE_SERVER: WebSocket base URL for the relay server, for examplews://localhost:7891orwss://your-domain
When creating an SSH connection, choose:
Via agent: <agent name>
instead of the default direct route.
Important:
- the
hostfield must be resolvable from the machine running the agent - the agent machine must itself be able to reach the SSH target
npm install pulls Puppeteer, which downloads a bundled Chromium build. If
it fails:
- Disk full or low memory: Puppeteer needs ~300 MB free.
- Behind a proxy: set
HTTPS_PROXYandPUPPETEER_DOWNLOAD_BASE_URLto a mirror. - Don't want Chromium downloaded: set
PUPPETEER_SKIP_DOWNLOAD=truein.env. The remote browser feature will not work until Chromium is installed manually. - Need a system Chrome: set
PUPPETEER_EXECUTABLE_PATH=/path/to/chromein.env.
This repo runs two app processes in development:
- Next.js frontend on
http://localhost:3000 - Express/WebSocket server on
http://localhost:7891
The frontend handles UI and local API routes. The Express server handles:
- SSH WebSocket sessions
- Browser session control
- agent relay connections
- online agent status checks
npm run dev
npm run build
npm run start
npm run lint
npm run db:push
npm run db:migrate
npm run db:studio- src/App.jsx: top-level workspace layout
- src/components/Canvas.jsx: infinite canvas wrapper
- src/components/WindowFrame.jsx: draggable/resizable window shell
- src/apps/registry.tsx: app registry including SSH terminal wiring
- src/components/SSHPanel.tsx: saved SSH connections UI
- src/components/AgentPanel.tsx: create/list agent UI
- server/index.ts: Express + WebSocket relay server
- server/lib/ssh.ts: SSH session handling and agent proxy logic
- agent/index.js: relay agent process
Infinite has three components, plus an optional agent:
| Component | Process | Default port | Role |
|---|---|---|---|
frontend |
Next.js 16 (App Router) | 3000 (dev) / 7890 (docker) |
UI, API routes, Prisma client |
server |
Express + WebSocket | 7891 |
SSH sessions, browser control, agent relay, status checks |
db |
PostgreSQL 16 | 5432 |
Persisted state (connections, layouts, notes, projects, agents) |
agent |
Standalone Node.js | outbound WS | Optional proxy that runs where the SSH target is reachable |
The frontend and server can run on the same host or different hosts.
The server is the only component that needs direct network access to
SSH targets. The frontend only talks to the server over HTTP and
WebSocket.
For local single-host Docker, see Quick Start. For local single-host Node.js, see Manual Install.
vercel.json is preconfigured to deploy the Next.js app to Vercel. Point
NEXT_PUBLIC_WS_URL at the public URL of the relay server. Run the relay
server on a host that:
- has a stable public address (or is reachable through Tailscale, Cloudflare Tunnel, WireGuard, etc.)
- can reach the SSH targets you want to expose
Suggested setup:
-
Deploy the frontend:
vercel deploy --prod
-
Run the relay server on a separate machine (VPS, home lab, etc.) with
Dockerfile.server. The example runs on port7891:docker build -f Dockerfile.server -t infinite-server . docker run -d \ --name infinite-server \ -p 7891:7891 \ -e DATABASE_URL=... \ -e DIRECT_URL=... \ -e ENCRYPTION_SECRET=... \ -e PORT=7891 \ -e ALLOWED_ORIGINS=https://your-vercel-app.vercel.app \ infinite-server -
Set
NEXT_PUBLIC_WS_URLin the Vercel project towss://relay.example.com. -
If the relay host is behind Tailscale, use the Tailscale hostname in
NEXT_PUBLIC_WS_URLso both ends speak over the tailnet.
A minimal nginx config that fronts the frontend on 443 and proxies
WebSocket upgrades to the relay on 7891:
server {
listen 443 ssl http2;
server_name infinite.example.com;
ssl_certificate /etc/letsencrypt/live/infinite.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/infinite.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:7890;
}
location /ws/ {
proxy_pass http://127.0.0.1:7891;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}Adapt the WebSocket path prefix to whatever server/index.ts listens on.
infinite.service and ecosystem.config.cjs are provided for running
next start and the relay under PM2 or systemd on a single host. Edit
infinite.service to match your install path and user before enabling it:
sudo cp infinite.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now infinite- Infinite is currently a single-user app. There is no auth flow; it assumes it is running on a trusted network or behind a reverse proxy that handles authentication.
- SSH credentials (passwords and private keys) are encrypted at rest with
ENCRYPTION_SECRET. Treat that secret like a database password: if you lose it, saved credentials cannot be recovered. - The relay server can open arbitrary TCP connections to any host you configure. Restrict network access to the relay (firewall, Tailscale ACL, Cloudflare Tunnel policy) so only trusted clients can reach it.