Run a FrogTalk Lilypad (node)

A Lilypad is a FrogTalk node. Self-host one β€” a federated chat server β€” on a fresh Linux VPS: CLI setup wizard, nginx, systemd, and sync with the public mesh anchored at frogtalk.app.

Pre-alpha: expect breaking changes between releases. Pin a git tag or verify SHA-256 from the release feed before production use.

Full guide (GitHub): docs/NODE_INSTALL.md Β· Typical path: /opt/frogtalk

Pre-alpha: FrogTalk is experimental. We do not guarantee uptime, data safety, or API stability. Deploy from the master branch for production nodes. See security & disclosure before exposing a node to the public internet.
Secrets: Never commit .env, SSH passwords, or federation tokens. Use SSH keys for VPS login. Placeholders: <YOUR_VPS_IP>, <YOUR_DOMAIN>.

Quick install (copy-paste)

Run on a fresh Ubuntu/Debian VPS as a user with sudo. Replace <YOUR_VPS_IP> (or use https://chat.<YOUR_DOMAIN> once DNS works).

sudo apt update
sudo apt install -y git python3 python3-venv python3-pip curl nginx php-fpm php-curl ufw sqlite3

sudo adduser --disabled-password --gecos "" deploy
sudo mkdir -p /opt/frogtalk && sudo chown deploy:deploy /opt/frogtalk

sudo -u deploy git clone https://github.com/deadinternetfox/frogtalk.git /opt/frogtalk
cd /opt/frogtalk

export PUBLIC_URL="http://<YOUR_VPS_IP>"
export FROGTALK_SERVER_NAME="My FrogTalk Node"
# Same token as on FrogTalk Main (frogtalk.app) β€” generate once on Main:
export FROGTALK_FEDERATION_TOKEN="<openssl-rand-hex-32-on-main>"

sudo bash node/scripts/install.sh setup -y --install-dir /opt/frogtalk --public-url "$PUBLIC_URL"
sudo bash node/scripts/install.sh systemd -y --install-dir /opt/frogtalk

set -a && source /opt/frogtalk/.env && set +a
curl -sk "$PUBLIC_URL/api/ping" | python3 -m json.tool
curl -sS http://127.0.0.1/board/api/info | python3 -m json.tool
bash node/scripts/install.sh federation -y --install-dir /opt/frogtalk --public-url "$PUBLIC_URL"

sudo ufw allow OpenSSH && sudo ufw allow 80/tcp && sudo ufw allow 443/tcp
sudo ufw --force enable

curl -sS https://frogtalk.app/api/network/servers | python3 -m json.tool
grep ADMIN_PASSWORD /opt/frogtalk/.env

setup -y (root) installs board nginx, auto HTTPS for http:// IPs (self-signed), then updates PUBLIC_URL to https://. See HTTPS.

Open $PUBLIC_URL/app, log in as admin (password in .env), rotate the password.

🐳 Run with Docker (fastest)

The backend node ships as a container. This runs the FrogTalk app + imageboard on :8080; put nginx or a Cloudflare Tunnel in front for TLS and your domain. Data persists in named volumes.

git clone https://github.com/deadinternetfox/frogtalk.git
cd frogtalk

# optional: your public URL once TLS/DNS is set
export PUBLIC_URL="https://chat.<YOUR_DOMAIN>"

docker compose up -d            # builds the image + starts the node
docker compose logs -f          # watch it boot
curl -sS http://127.0.0.1:8080/healthz   # {"ok":true}

Then point nginx/Cloudflare at 127.0.0.1:8080, open $PUBLIC_URL/app, log in as admin and rotate the password. Join the mesh with federation. The image is defined in node/Dockerfile and docker-compose.yml; a multi-arch image can also be published to ghcr.io/deadinternetfox/frogtalk (run the GHCR workflow).

Persistence: volumes ft-data (DB + federation identity), ft-secrets, ft-uploads, ft-board. Update with git pull && docker compose up -d --build.

Prerequisites

ItemRecommendation
VPS1–2 vCPU, 2–4 GB RAM, 20+ GB disk
OSDebian 12 / Ubuntu 22.04+ with systemd
Python3.10+ (3.11+ recommended)
DNSOptional β€” A/AAAA for chat.<YOUR_DOMAIN>, or IP-only http://<YOUR_VPS_IP>
Ports22 (SSH), 80/443 (nginx) β€” app stays on 127.0.0.1:8080

Install wizard = bash CLI (node/scripts/install.sh), not a browser UI.

1) Bootstrap the VPS

ssh-copy-id -i ~/.ssh/id_ed25519.pub root@<YOUR_VPS_IP>
ssh root@<YOUR_VPS_IP>

sudo apt update
sudo apt install -y git python3 python3-venv python3-pip curl nginx \
  certbot python3-certbot-nginx ufw sqlite3

sudo adduser --disabled-password --gecos "" deploy
sudo mkdir -p /opt/frogtalk && sudo chown deploy:deploy /opt/frogtalk

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

2) Clone repo + run setup wizard

sudo -u deploy git clone https://github.com/deadinternetfox/frogtalk.git /opt/frogtalk
cd /opt/frogtalk

export PUBLIC_URL="https://chat.<YOUR_DOMAIN>"
export FROGTALK_SERVER_NAME="My FrogTalk Node"

# Interactive menu
bash node/scripts/install.sh

# Non-interactive (recommended)
bash node/scripts/install.sh setup -y --install-dir /opt/frogtalk --public-url "$PUBLIC_URL"
bash node/scripts/install.sh federation -y --install-dir /opt/frogtalk --public-url "$PUBLIC_URL"
sudo bash node/scripts/install.sh systemd -y --install-dir /opt/frogtalk

The wizard creates venv/, writes .env (HOST=127.0.0.1, PORT=8080), and symlinks node/data, node/.env, node/secrets. A real node/data directory (not a symlink) causes an empty DB β€” re-run setup or federation join to repair.

Admin: user admin on first boot. Password is in /opt/frogtalk/.env as ADMIN_PASSWORD (auto-generated if left blank).

Account sync (visiting another node)

Users who log in on a community node (not their home) can import joined channel settings (themes, slowmode, forwarding lock), DM thread prefs, friends/following, profile theme/CSS, client prefs (Tor, sounds), and FrogSocial posts from their home server under Settings β†’ Network β†’ Re-sync from home. Sync is automatic on login when incomplete. Exports are Ed25519-signed by the home node and verified on import. Federated directory channels can be joined with Join without a prior sync row (metadata from the index only).

# Optional tuning in /opt/frogtalk/.env (defaults keep features on)
FROGTALK_SYNC_PERSIST=1
FROGTALK_SYNC_BIND_HOME=1
FROGTALK_SYNC_VERIFY_EXPORT=1
FROGTALK_SYNC_SIGN_EXPORT=1
FROGTALK_SYNC_REQUIRE_EXPORT_SIG=1
FROGTALK_SYNC_LOGIN_RESUME=1
FROGTALK_SYNC_PAGINATION=1
# FROGTALK_SYNC_STALE_HOURS=24

Calls: DM voice/video between nodes uses home-server signaling. Travelers currently connected to your node are rung locally even when homed elsewhere. Enable FROGTALK_FEDERATION_CALLS_ENABLED=1 and publish TURN URLs. See API docs (Federation).

Preferred node (app): users save their network choice with PATCH /api/auth/client-prefs (preferred_node_url, optional prefer_onion) β€” included when they re-sync from home. See API β†’ Auth.

3) Federation β€” join the mesh

bash node/scripts/install.sh federation -y --install-dir /opt/frogtalk \
  --public-url "$PUBLIC_URL"

# Or directly:
bash node/scripts/node_federation_join.sh --install-dir /opt/frogtalk -y \
  --public-url "$PUBLIC_URL"

Pulls the official directory, announces this node on the hub (POST …/servers/register), imports peers, and pins Ed25519 keys from each peer’s /api/network/status (run after frogtalk is up).

Token: set the same FROGTALK_FEDERATION_TOKEN on FrogTalk Main and on this node (openssl rand -hex 32 on Main; never commit). Without it, join warns and you won’t appear on frogtalk.app until you set the token and re-run federation.

# Verify global listing (authoritative feed on Main):
curl -sS https://frogtalk.app/api/network/servers | python3 -m json.tool

# Your own node's /api/network/servers always includes local identity β€”
# that is NOT proof you are listed on frogtalk.app.
NameRole
FrogTalk Mainhttps://frogtalk.app β€” production hub / official directory
FrogTalk Tor MirrorOnion mirror β€” listed in network picker when configured

Mesh config (operators): federation seed peers and URL repairs are defined in FROGTALK_FEDERATION_MESH_FILE (copy node/deploy/federation-mesh.example.json). Hub VPS: FROGTALK_FEDERATION_DIRECTORY_HUB=1.

FROGTALK_FEDERATION_ENABLED=1
FROGTALK_FEDERATION_REQUIRE_SIGS=1
FROGTALK_OFFICIAL_DIRECTORY_URL=https://frogtalk.app/api/network/servers
FROGTALK_FEDERATION_TOKEN=
# Hub only (optional):
# FROGTALK_FEDERATION_DIRECTORY_HUB=1
# FROGTALK_FEDERATION_MESH_FILE=/opt/frogtalk/federation-mesh.local.json
FROGTALK_TOR_ENABLED=0
PUBLIC_URL=https://chat.<YOUR_DOMAIN>
FROGTALK_SERVER_NAME=My FrogTalk Node
FROGTALK_HOME_PAGE=main
ALLOWED_ORIGINS=https://chat.<YOUR_DOMAIN>
FROGTALK_AUTO_UPDATE_ENABLED=0
# Clearnet + /board/ behind Cloudflare tunnel (typical):
PORT=8000
FROGTALK_NGINX_TUNNEL_LISTEN=1

4) HTTPS (stop β€œNot secure” on HTTP)

http://<IP> is always labeled not secure. After setup, run:

sudo bash node/scripts/install.sh ssl -y --install-dir /opt/frogtalk

The setup wizard now offers a recommended HTTPS option: free trusted cert (Let's Encrypt via certbot, for domains), plus self-signed fallback. IP-only nodes still use self-signed with HTTP→HTTPS redirect. Use https://<YOUR_VPS_IP>/ (accept trust prompt once), or map a domain to get a clean padlock. If certbot fails, the SSL helper prints free CLI alternatives (ZeroSSL/Buypass via acme.sh).

nginx terminates TLS and proxies to 127.0.0.1:8080.

server {
    listen 80;
    server_name chat.<YOUR_DOMAIN>;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

sudo ln -sf /etc/nginx/sites-available/frogtalk /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d chat.<YOUR_DOMAIN>

Reference: node/deploy/nginx.conf (wizard: install_board_nginx.sh). Match PORT in .env with proxy_pass.

Frog Channel (/board/)

PHP imageboard on the same origin. sudo … setup -y installs nginx + php-fpm for /board/ and sets title from FROGTALK_SERVER_NAME.

sudo bash node/scripts/install.sh board-nginx --install-dir /opt/frogtalk
curl -sS http://127.0.0.1/board/api/info | python3 -m json.tool
bash node/scripts/install.sh federation -y --public-url "$PUBLIC_URL"

Peer board links appear after federation when php-curl is installed. Operators edit identity in Server Admin or /board/admin.

6) Verify

bash node/scripts/install.sh status --install-dir /opt/frogtalk
curl -sS "$PUBLIC_URL/api/ping"
curl -sS "$PUBLIC_URL/api/network/status" | python3 -m json.tool
journalctl -u frogtalk -n 40 --no-pager

Open $PUBLIC_URL/app, register a test user. In Settings β†’ Network, probe peers.

Server Admin shows a warning banner when PUBLIC_URL has no domain or no HTTPS, with fix steps. Use β€œHide until fixed” to dismiss for the current page session; it returns on next load until corrected. Raw-IP PUBLIC_URL still exposes the node IP to users and federation peers until a domain + HTTPS is configured.

Troubleshooting

SymptomFix
no such table / empty appFix node/data symlink β†’ /opt/frogtalk/data
Works on :8080, not on domainAlign PORT and nginx proxy_pass
federation -y ignored flagsUse current install.sh β€” flags must follow the subcommand: federation -y --public-url …
Peers listed, no syncRe-run federation -y after service is running
Traveler empty feed on foreign nodeSettings β†’ Network β†’ Re-sync; check sync status for errors or partial import
Call does not ring on foreign nodeCallee may be homed elsewhere β€” they must be online here or at home; friends-only gate default
Channel name clash after syncLocal channel kept; home channel listed in federated directory only β€” join via directory may return name_collision
Friend theme missing on travel nodeRe-sync from home; profiles merge federation_user_profiles cache
Not on frogtalk.app directoryMatch FROGTALK_FEDERATION_TOKEN on Main + node; re-run federation -y
CORS errorsAdd your URL to ALLOWED_ORIGINS
/board/ OK but /app 502nginx must not listen 8080 when app uses PORT=8080 β€” re-run board-nginx (wizard strips tunnel port by default)
/board/ 404Re-run sudo bash node/scripts/install.sh board-nginx; check php-fpm + nginx test
Board pills / curl_initsudo apt install php-cli php-curl; re-run federation
systemd permission deniedsudo chown -R deploy:deploy /opt/frogtalk; sudo bash node/scripts/install.sh systemd -y

Security reports: frogtalk.app/security

Tor / onion (optional)

FROGTALK_TOR_ENABLED=1
FROGTALK_ONION_URL=http://yourhiddenservice.onion
FROGTALK_TOR_SOCKS_PROXY=socks5h://127.0.0.1:9050
bash node/scripts/install.sh federation -y \
  --onion-url http://yourhiddenservice.onion --public-url ""

Clients open http://….onion/app. Clearnet hubs need Tor SOCKS for onion peers.

7) Secure your node (free, do all of these)

On a public Linux server, hardening, firewalling, patching and intrusion detection matter far more than antivirus. None of this costs anything. Full reference: docs/NODE_SECURITY.md in the repo.

Firewall β€” default-deny inbound

apt-get install -y ufw
ufw allow 22/tcp            # your SSH port
ufw allow 80/tcp; ufw allow 443/tcp
ufw allow 8080/tcp         # nginx app/board upstream (skip if Cloudflare-tunnelled)
# coturn (only if this node runs calls):
ufw allow 3478/tcp; ufw allow 3478/udp; ufw allow 49152:65535/udp
ufw default deny incoming; ufw default allow outgoing
ufw --force enable

Don't lock yourself out: allow your SSH port before enable, and arm an auto-revert while you test β€” systemd-run --on-active=300 --unit=ufw-safety --collect ufw --force disable, then open a fresh SSH session and systemctl stop ufw-safety.timer once it works.

Brute-force defence + intrusion detection

apt-get install -y fail2ban && systemctl enable --now fail2ban

# CrowdSec: behavioural detection + crowd-sourced blocklists + a bouncer that bans
curl -s https://install.crowdsec.net | sh
apt-get install -y crowdsec crowdsec-firewall-bouncer-nftables
cscli collections install crowdsecurity/linux crowdsecurity/sshd crowdsecurity/nginx
systemctl enable --now crowdsec crowdsec-firewall-bouncer

Gotcha: CrowdSec's local API defaults to 127.0.0.1:8080, which clashes with FrogTalk's nginx. Move it to 127.0.0.1:8083 in /etc/crowdsec/config.yaml, local_api_credentials.yaml and the bouncer yaml, then restart CrowdSec.

App-layer bouncers β€” block at nginx and the board (free)

The firewall bouncer drops attackers at the network layer. Add the nginx bouncer to block or CAPTCHA bad IPs at the proxy β€” this covers /app and the /board/ imageboard β€” and (optionally) a PHP bouncer for defence-in-depth inside the board:

# nginx bouncer β€” registers itself with the local API
apt-get install -y crowdsec-nginx-bouncer
# if you relocated the LAPI off :8080 (see gotcha above), point the bouncer at it:
sed -i 's#API_URL.*#API_URL = http://127.0.0.1:8083#' /etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf
nginx -t && systemctl reload nginx        # ALWAYS test before reload

# make CrowdSec read the board/app access log so it can detect abuse there
cscli collections install crowdsecurity/nginx
cscli bouncers list                          # confirm the bouncer is registered + valid

Cloudflare: if your zone is on Cloudflare, the Cloudflare Workers bouncer blocks at the edge before traffic reaches the box (needs a Workers-scoped API token). CTI enrichment: add a free CrowdSec console CTI key to score IP reputation. Verify everything with cscli bouncers list and cscli metrics.

Patch automatically + run least-privilege

apt-get install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades

Run FrogTalk as a non-root user (the installer uses deploy; the systemd unit should never be User=root). Prefer SSH keys and, once your key works, PasswordAuthentication no.

Strip the box to "just a FrogTalk node"

Fewer packages = smaller attack surface. Remove desktop/bloat (apt purge snapd xserver-common), disable hardware daemons useless on a VPS (systemctl disable --now ModemManager fwupd multipathd udisks2 upower), and remove anything that isn't FrogTalk. If you find Docker containers or VPN/proxy software you didn't install, treat it as a possible compromise.

Danger Zone β€” node self-destruct

The /server admin panel has a Danger Zone β†’ Nuke this node for decommission / seizure / confirmed-compromise. It securely shreds the database, secrets, keys, uploads and Tor identity, removes the install itself, best-effort wipes free RAM, then powers off. It's gated by your admin session + a fresh PIN (or password) + typing the node's exact name. Install the helper for the full wipe:

install -o root -g root -m 0500 node/deploy/frogtalk-nuke.sh /usr/local/sbin/frogtalk-nuke.sh
install -o root -g root -m 0440 node/deploy/frogtalk-nuke.sudoers /etc/sudoers.d/frogtalk-nuke
visudo -cf /etc/sudoers.d/frogtalk-nuke
apt-get install -y secure-delete   # sdmem / sfill for RAM + free-space wipe

Honest limits: on SSD/NVMe an in-place overwrite isn't guaranteed (wear-levelling) β€” pair it with a provider disk-destroy; "shred RAM" only covers free RAM + cache, and power-off clears the rest.

Updates

bash node/scripts/install.sh update
bash node/scripts/install.sh update-apply -y
bash node/scripts/install.sh federation -y --public-url "$PUBLIC_URL"

Set FROGTALK_RELEASE_SIGNERS before FROGTALK_AUTO_UPDATE_ENABLED=1.

What your node serves (recent app features)

The web client at $PUBLIC_URL/app includes more than classic chat β€” operators should know what users will see after federation sync.

AreaSummary
EncryptionSignal Protocol DMs; AES-256-GCM private channels with AAD binding and key rotation on ban/kick
Frog SocialPosts, Reels, stories, reactions, friend wall
Frog ChannelPHP imageboard at /board/ β€” threads, greentext, mod tools
BridgesDiscord & Telegram two-way sync (public channels only; blocked for E2EE private rooms)
Music roomsSynced YouTube / Spotify / SoundCloud listening
CallsWebRTC voice/video; cross-node DM calls with FROGTALK_FEDERATION_CALLS_ENABLED=1 + TURN
Account syncTravelers re-sync signed exports from home (channels, DMs prefs, friends, themes, FrogSocial)
18+ labelsVoluntary public-channel content warnings with session age gate (not automated scanning)
Federation UIRelease feed SHA-256 checks, trust badges, onion handoffs to /app

API reference: frogtalk.app/docs/api Β· Encryption detail: docs/SECURITY_MODEL.md

Project layout

/opt/frogtalk/
β”œβ”€β”€ .env
β”œβ”€β”€ data/
β”œβ”€β”€ secrets/
β”œβ”€β”€ venv/
└── node/
    β”œβ”€β”€ main.py
    β”œβ”€β”€ static/       # /app Β· /docs/node
    β”œβ”€β”€ board/
    └── scripts/      # install.sh Β· federation join