Local-only WebRTC audio intercom for Fire tablets. Next.js + Tailwind (client) and Socket.io signaling (server). Tablets hit http://LXC_IP:3000/intercom.
FYI, if we are talking code, we are likely on my macos env for dev. Code is deployed via git to remote LXC on my LAN.
Setup Summary: A WebRTC audio intercom for Fire tablets: Next.js 16 (App Router) + React 19 + Tailwind CSS 4 client (client/ui) and an Express + Socket.io signaling server (server). Development runs on macOS (UI on 3000, signaling on 3001); production deploys to Proxmox LXC containers (Debian 12/13) via setup-lxc.sh, which installs Node.js LTS, builds the Next.js app, configures environment variables (NEXT_PUBLIC_SIGNALING_URL), and runs both services under pm2 with systemd integration. The client uses WebRTC peer connections (Google STUN) with Socket.io signaling, Web Audio API highpass filtering, push-to-talk controls, and connection state monitoring. Tablets access http://LXC_IP:3000/intercom; optional Caddy HTTPS proxy supports macOS/iOS testing. The codebase uses TypeScript, minimal dependencies, and a monorepo structure with separate client/server packages.
- Node 20+ (Nodesource LTS repo is fine).
- npm.
- Git.
- Develop on macOS: Edit code locally, test with
npm run dev. - Push to GitHub:
git push origin main(or your branch). - Deploy on LXC: Clone from GitHub and run
setup-lxc.sh.
cd /Users/chase/Code/intercom
cp client/ui/env.example client/ui/.env.local # set NEXT_PUBLIC_SIGNALING_URL to http://localhost:3001
npm --prefix server install
npm --prefix client/ui install
npm --prefix server run dev # port 3001
npm --prefix client/ui run dev # port 3000
# browser: http://localhost:3000/intercom- Client
.env.local:NEXT_PUBLIC_SIGNALING_URL=http://LXC_IP:3001
- Server env (pm2/systemd):
PORT=3001ALLOWED_ORIGINS=http://LXC_IP:3000
-
Create LXC using community-scripts helper:
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/debian.sh)"Choose Debian 12 or 13, unprivileged, static LAN IP, 1 vCPU, 1–2GB RAM, 15–20GB disk.
-
Inside LXC (as root):
export LXC_IP=192.168.4.226 # your LXC's LAN IP # Install git if not present (script will install node/pm2) apt update && apt install -y git git clone https://github.com/chasecee/intercom.git /opt/intercom cd /opt/intercom chmod +x setup-lxc.sh ./setup-lxc.sh
The script installs Node.js/pm2, installs deps, builds Next.js, creates .env.local, and starts both services with pm2.
After pushing changes from macOS:
# On LXC
cd /opt/intercom
git pull
./deploy.shThe deploy script will:
- Install server dependencies (
npm ciinserver/) - Install client dependencies (
npm ciinclient/ui/) - Build the client application (
npm run build) - Restart both pm2 services (
intercom-signalandintercom-ui)
For manual deployment:
cd /opt/intercom
git pull
cd server && npm ci && cd ..
cd client/ui && npm ci && npm run build && cd ../..
pm2 restart intercom-signal
pm2 restart intercom-uipm2 delete all
rm -rf /opt/intercom
git clone https://github.com/chasecee/intercom.git /opt/intercom
cd /opt/intercom
export LXC_IP=192.168.4.226
./setup-lxc.sh- URL:
http://LXC_IP:3000/intercom(orhttps://if you set up Caddy below) - Keep screen on, kiosk mode, auto-reload in Fully Kiosk.
macOS Safari/Chrome and iOS Safari block getUserMedia on HTTP for non-localhost URLs. Fire tablets (Android) work fine with HTTP, but if testing on macOS/iOS, use HTTPS. Browsers also block mixed content (HTTPS page connecting to HTTP WebSocket), so we proxy signaling through HTTPS too.
# On LXC, install Caddy
apt install -y debian-keyring debian-archive-keyring apt-transport-https gnupg
mkdir -p /etc/apt/keyrings
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /etc/apt/keyrings/caddy-stable.gpg
echo "deb [signed-by=/etc/apt/keyrings/caddy-stable.gpg] https://dl.cloudsmith.io/public/caddy/stable/deb/debian any-version main" >/etc/apt/sources.list.d/caddy-stable.list
apt update && apt install -y caddy
# Create Caddyfile (proxy Socket.io WebSocket through HTTPS)
# Replace 192.168.4.226 with your LXC_IP
cat >/etc/caddy/Caddyfile <<EOF
192.168.4.226 {
reverse_proxy /socket.io/* localhost:3001
reverse_proxy /intercom* localhost:3000
reverse_proxy /_next/* localhost:3000
reverse_proxy / localhost:3000
tls internal
}
EOF
# Update UI env to use HTTPS (no port, Caddy proxies /socket.io/*)
# Replace 192.168.4.226 with your LXC_IP
cat >/opt/intercom/client/ui/.env.local <<EOF
NEXT_PUBLIC_SIGNALING_URL=https://192.168.4.226
EOF
# Update server CORS to allow HTTPS origin
# Replace 192.168.4.226 with your LXC_IP
pm2 delete intercom-signal
cd /opt/intercom/server
PORT=3001 ALLOWED_ORIGINS=https://192.168.4.226 pm2 start npm --name intercom-signal -- run start
pm2 save
# Rebuild UI with new env
cd /opt/intercom/client/ui
npm run build
pm2 restart intercom-ui
# Start Caddy
systemctl enable caddy
systemctl start caddy
# Verify Caddy is listening on 443
ss -tlnp | grep caddyThen access https://192.168.4.226/intercom. Browsers will show a security warning about the self-signed certificate (Caddy's tls internal); click "Advanced" → "Proceed" to accept it. No certificate copying needed—this is all server-side.
Note: The signaling WebSocket is proxied through Caddy on HTTPS (wss://), so both UI and signaling use HTTPS. For Fire tablets, HTTP works fine (no HTTPS needed).
Check service status:
pm2 ls
pm2 logs intercom-signal --lines 50
pm2 logs intercom-ui --lines 50Test signaling server:
curl http://localhost:3001/health # should return {"status":"ok"}Test UI locally (macOS) connecting to LXC signaling:
# On macOS, update local .env.local to point to LXC
cd /Users/chase/Code/intercom/client/ui
cat >.env.local <<EOF
NEXT_PUBLIC_SIGNALING_URL=http://192.168.4.226:3001
EOF
npm run dev
# Then access http://localhost:3000/intercomCaddy not starting:
- Check Caddyfile syntax:
caddy validate --config /etc/caddy/Caddyfile - Check logs:
journalctl -u caddy -n 50 - Verify ports:
ss -tlnp | grep caddy(should show 80 and 443)
WebSocket not connecting:
- Ensure Caddyfile proxies
/socket.io/*tolocalhost:3001 - Check CORS allows the correct origin
- Verify signaling URL in
.env.localmatches your setup (HTTP vs HTTPS)
Services not starting after reboot:
# Check if pm2 systemd service exists
systemctl status pm2-root
# If not configured, set it up:
pm2 startup
# Then run the command it outputs (as root)
# Or manually start services:
pm2 resurrect
pm2 save
# Verify services are running:
pm2 ls
pm2 logs