Tephro · Guides

Running a Tephro server

One binary, one directory, one port. This is the whole procedure; if a step here does not work, that is a bug worth reporting.

1. Get the binary

Download the build for your platform from the releases page (five targets: Linux amd64/arm64, Windows amd64, macOS amd64/arm64), or build it yourself with Go 1.26+:

git clone https://gitlab.com/AuHunt/tephro
cd tephro && make server-build   # → dist/server/tephro

Check the download: every release ships SHA256SUMS and its OpenPGP signature SHA256SUMS.asc, made with the maintainer's signing key on a hardware token. With gpg installed:

curl -fsSL https://gitlab.com/AuHunt.gpg | gpg --import
gpg --verify SHA256SUMS.asc SHA256SUMS
sha256sum -c SHA256SUMS

The second line must report a good signature from a key with the primary fingerprint 3174 0E3B 30C6 597B 5578 FE09 A760 51A5 9D84 E725; the third proves the binary matches the checksums. The same key is embedded in the desktop client, which runs the same two checks before installing an update. Nothing else is needed on the box: no database server, no reverse proxy, no TURN daemon.

2. Run it once

./tephro

In a terminal, the first run is a short setup wizard: instance name, listen address, TLS mode, admission mode, whether to mint the owner invite, your public IP (detected with one request to Mullvad's echo service, typed, or skipped), and which address the owner invite link should carry — the public one, this machine (127.0.0.1, when the client runs on the same box), or one you type. **On Linux, as an ordinary user, the very first thing you may see instead is permission denied on port 443** — that is section 3, one command, and the wizard walks you through it when there is a terminal. It writes tephro.toml into the data directory, prints the owner invite, and then the server goes to the background and your shell comes back with the process id and the log path. tephro tui shows the live status view whenever you want it; tephro admin shutdown stops the server. The data directory is the one place the server writes: by default ~/.local/share/tephro/server on Linux (%LOCALAPPDATA%\Tephro\server on Windows, ~/Library/Application Support/Tephro/server on macOS — the desktop client keeps its own files beside it under the same root), or wherever --data-dir / TEPHRO_DATA_DIR points; TEPHRO_HOME moves the whole root. Run the binary from any directory and it finds the same instance. Every answer is also a config key, so a headless box (a container, a service manager) needs no wizard: it boots on defaults and logs what to change.

The one thing to know: the owner is whoever registers with the owner invite. Mint it in the wizard or later with

./tephro admin invite --owner

and register with that key from the desktop client. If the owner disappears — the person is unreachable, their machine is gone — mint a replacement from the box: tephro admin invite --owner --force. Whoever registers with it becomes the owner and the previous owner loses the flag (the audit log records it). Without --force the command refuses while an active owner with keys exists and points at tephro admin owner <username>, which moves ownership to an existing account instead. Every later invite comes from the client, or from tephro admin invite: a plain one is a personal key (one use, admits directly); --public makes a public link (unlimited uses) whose joiners wait for your approval unless you pass --approval no or turn the public_invite_approval setting off. tephro admin approvals lists who is waiting; approve <name> and deny <name> decide. Every invite is printed with a second, same machine link for a client running on this box.

3. Ports

The server listens on TCP 443 (HTTPS, the client's websocket, and the ICE-TCP fallback) and UDP 443 (all voice and video, every call, one port). Nothing else. Members' networks are least likely to block 443, which is why it is the default and why you should keep it if you can.

Linux needs one grant to bind a port below 1024. Pick whichever fits how you run it:

How you run itWhat to do
By handsudo setcap cap_net_bind_service=+ep ./tephro — repeat after replacing the binary
systemdUse server/deploy/tephro.service; it carries AmbientCapabilities=CAP_NET_BIND_SERVICE
ContainerPublish the port (-p 443:443 -p 443:443/udp). Rootful docker or podman: nothing else. Rootless podman refuses ports below 1024: sudo sysctl net.ipv4.ip_unprivileged_port_start=443 (and a file in /etc/sysctl.d/ to keep it), or publish a high host port (-p 8443:443 -p 8443:443/udp) and have the router forward external 443 to it — the server inside still binds 443, so this is the one place a port translation works
Behind a home routerGrant the bind as above and forward external 443 → 443, TCP and UDP, to this machine. Set public_ip — the wizard detects it, or run tephro admin public-ip. Details below

The wizard test-binds your chosen port and walks through these on a permission error. Windows and macOS do not restrict low ports.

Behind a home router

The port must be the same on both sides of the router. The server advertises the port its socket is bound to, and the client aims at the port in the invite link, so a rule that translates external 443 to an internal 8443 leaves every call knocking on a port nobody forwarded. Two shapes work:

Then public_ip. The wizard's detect asks an echo service once and writes the literal address; tephro admin public-ip does the same later. Without it the server's offers name only its LAN address and calls never find the way in. Home addresses change: when yours does, run it again and hand out fresh links, or point a DNS name here and set domain. Members on your own LAN get a link with the LAN address — tephro admin invite --host 192.168.1.10 — because most home routers do not route a LAN client out and back in.

Before inviting anyone, open https://<public_ip> from a phone on mobile data. The certificate warning is expected (self-signed); the page behind it is the instance's own. If nothing loads, check the forward rule and this machine's firewall, then whether your ISP puts you behind carrier-grade NAT: the router's WAN address is in 100.64.0.0/10 or differs from the detected public IP. Behind CGNAT no forward works and a VPS is the answer.

4. TLS and the fingerprint

By default the server generates a self-signed certificate on first run and prints its fingerprint. The desktop client pins that fingerprint — it does not need a certificate authority, so a bare IP address works. The fingerprint travels inside every invite link (tephro://join?host=…&fp=sha256:…&key=…), so a member's first connection is verified rather than trusted.

If you have a DNS name and port 443 reachable from the internet, tls_mode = "acme" with domain = "chat.example.org" gets a Let's Encrypt certificate automatically. If a reverse proxy terminates TLS in front, tls_mode = "off" and trusted_proxies set to the proxy's address, so rate limits and logs see your members rather than the proxy (never set it on a server members reach directly).

5. Keeping it running

6. Day to day

TaskWhere
Invite people, approve join requests, roles, channels, moderation, audit logThe desktop client, as an administrator
Mint an invite from the box, list or revoke invites, add or remove a channel, transfer ownership, issue a recovery code, change the instance name or admission mode, see status, look up your public IP, stop the servertephro admin … (run tephro admin help). tephro admin invite --note "for Sam" mints a personal key (--public a link, --local also prints the loopback link for a client on this box); tephro admin invites lists the keys that can still admit someone (--all for spent and revoked ones); tephro admin revoke <key> ends one; tephro admin shutdown stops the server
The shared tag listtephro admin tags lists it; tags add NAME, tags rename OLD NEW (--merge folds two entries into one), tags delete NAME; the status view's 6 screen does the same. max_tags (200) caps it
Park idle voice membersafk_timeout_minutes (10) is how long a silent member sits in voice before the server moves them to the AFK channel and shows them away; 0 turns it off. Needs an AFK channel (tephro admin channel add AFK --afk)
Helptephro -h lists the commands; tephro admin -h the subcommands; tephro admin <cmd> -h one subcommand's flags
Flag members on VPN or hosting addressestephro admin vpn-list update fetches the range list (one request, only when you run it; vpn_list_url in the config), then tephro admin vpn-list and the members screen mark them vpn. A flag for you to weigh, never a block. ip_ban_hours makes a ban also refuse the person's last addresses for a while; it is 0 unless you set it
Disconnect, time out, ban or unban someone without a client`tephro admin disconnecttimeoutbanunban <username> [--reason S] (timeout takes --for 2h or --until <RFC 3339>; without either it clears the timeout), then kill -HUP <pid> (or systemctl reload tephro) so the running server ends their connection; or select them on the status view's members screen (2) and press x, t, b or u` — that one applies at once. Disconnect lets them straight back in; timeout refuses their login until the time; ban keeps them out
Live view of connections, members, voice, storage, recent log; invitestephro tui against the running server, or tephro --tui to serve and watch in one terminal; 2 is the members screen (disconnect, timeout, ban, unban there), 4 the invites screen — n mints a key through a short form (who it is for, personal or public, approval, lifetime), r revokes, y copies the selected link — s the settings screen, q detaches (the server keeps running), Q stops the server after a confirm. Do not share your screen while an invite is on it
Change a settingAdmin → Server in the desktop client (every key, applied at once where that is safe, the rest marked restart), or press s in the status view (every setting, one line of explanation each, saved to tephro.toml and applied at once where that is safe — the notice says which keys need a restart), or tephro admin config set <key> <value> from a shell followed by kill -HUP <pid> (or systemctl reload tephro), or edit tephro.toml (every key is commented), or the matching --flag / TEPHRO_* variable and restart. tephro admin config lists them all with their current values

Attachment limits (max_upload_mb, user_quota_mb, instance_quota_mb), custom-emoji limits (max_emoji_kb, max_emoji), the pin limit (max_pins), video caps (video_cap_mbit, video_cap_channel_mbit) and the per-member send ceilings (max_camera_kbit, max_screen_kbit — what one camera or screen share may send, which clients honor) are yours to set; the defaults are generous and the server refuses honestly when a cap is hit rather than degrading everyone. disabled_codecs = "av1" takes a codec away from your members (vp8, vp9, av1); by default all three are allowed and each sender picks in the share dialog, so a member's stream is limited by nothing you did not turn on. Link previews are on by default: the server fetches a title and picture for links members post, so the link's host sees your server's address and never a member's. link_preview_hosts = "youtube.com, *.wikipedia.org" limits it to sites you name, and link_previews = false turns it off if you would rather it made no outbound requests at all. The pictures and videos it keeps for previews are limited to 10 MiB and 50 MiB each (max_preview_image_kb, max_preview_video_mb); anything larger is left out of the preview. Lower them on a small disk, because previews do not count against the attachment quotas, or set one to 0 for no limit. Direct messages between members are on by default and end-to-end encrypted, so your server holds only ciphertext; dms = false turns the relay off.

Something is wrong