- TypeScript 52.1%
- Svelte 38.2%
- JavaScript 5%
- HTML 1.7%
- CSS 1.4%
- Other 1.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo | ||
| .vscode | ||
| infra | ||
| landing | ||
| src | ||
| src-tauri | ||
| static | ||
| .gitignore | ||
| .npmrc | ||
| .prettierignore | ||
| .prettierrc | ||
| bun.lock | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| svelte.config.js | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
RyftBorn
A self-hosted, open-source Discord alternative. Own your community.
- Voids — Text channels
- Drifts — Voice/video channels with screen sharing
- Tethers — Direct messages
- Self-hosted — Your data, your server, your rules
Built with SvelteKit, PocketBase, LiveKit, and Tauri.
| RyftBorn | Discord Equivalent |
|---|---|
| Ryft | Server |
| Void | Text Channel |
| Drift | Voice Channel |
| Tether | DM |
Server Installation
Prerequisites
- A Linux server (VPS, dedicated, or local machine)
- Docker or Podman with Compose
- A domain name pointed at your server (for production with TLS)
- Ports 80, 443, 7881, 3478/udp, 50000-50200/udp open in your firewall
The examples below use
podman. Replace withdockerif that's what you have.
Podman setup (Linux only)
Podman needs a compose provider and Docker Hub registry configured:
pip3 install podman-compose
echo 'unqualified-search-registries = ["docker.io"]' >> /etc/containers/registries.conf
Quick Start (Local / Development)
This starts the full stack — PocketBase, the WebSocket server, LiveKit, the frontend (the web container, built from source, so the first start takes a few minutes), and a bundled Caddy for HTTPS.
git clone https://git.askdavis.com/AskDavis/RyftBorn.git
cd RyftBorn/infra
# Generate secrets and start containers
./setup.sh
setup.sh will:
- Prompt for a PocketBase admin email and password
- Generate a secret for LiveKit
- Write the
.envfile - Start the containers
- Create the PocketBase superuser account
- Enable username login
Once complete, the services are available at:
| Service | URL |
|---|---|
| PocketBase | http://localhost:8090 |
| PB Admin UI | http://localhost:8090/_/ |
| WebSocket | ws://localhost:7891 |
| LiveKit | ws://localhost:7880 |
| Caddy | https://localhost (landing) · https://app.localhost (app) |
Run the frontend for development:
cd ..
bun install
bun run dev
Open http://localhost:5173 and register your first account. The first user automatically becomes the server owner.
Production Deployment
Production is the same stack pointed at a real domain: Caddy handles reverse proxying with automatic TLS, and LiveKit's embedded TURN server handles NAT traversal for voice/video. The frontend is built inside Docker — no Bun or Node needed on the server.
Caddy is deliberately not part of the app stack's normal deploy. The stack only publishes loopback ports (the frontend on 127.0.0.1:7892, the landing page on 7893, plus PocketBase, the WS server and LiveKit signaling); a Caddy in front of them owns ports 80/443. That way redeploying RyftBorn restarts only RyftBorn's containers — never the proxy that any other site on the server depends on. The routing lives in infra/caddy/ryftborn.caddy, used by either:
- A Caddy installed on the host — use this if the server runs anything else (step 6a).
- The bundled Caddy container —
--profile edge, for a server that runs nothing but RyftBorn (step 6b).
1. Clone and configure
git clone https://git.askdavis.com/AskDavis/RyftBorn.git
cd RyftBorn/infra
2. Run setup
./setup.sh
3. Set your domain
Edit infra/.env and set your domain (both variables — DOMAIN is used by LiveKit's TURN server, DOMAIN_FRONTEND is what Caddy serves):
DOMAIN=ryftborn.example.com
DOMAIN_FRONTEND=ryftborn.example.com
Optionally add a Klipy API key for GIF support:
VITE_KLIPY_API_KEY=your_key_here
4. Enable TURN and external IP for LiveKit
Still in infra/.env, enable NAT traversal for production:
USE_EXTERNAL_IP=true
TURN_ENABLED=true
LIVEKIT_NODE_IP=
LiveKit's config is generated inside
docker-compose.ymlfrom these env vars — you don't need to edit any LiveKit config file. LeaveLIVEKIT_NODE_IPempty in production;use_external_iphandles IP discovery.
5. DNS setup
Point two DNS records at your server:
| Type | Name | Value |
|---|---|---|
| A | ryftborn.example.com |
Your server IP |
| A | app.ryftborn.example.com |
Your server IP |
infra/caddy/ryftborn.caddyalso servesmail.<domain>for a Stalwart mail server's web UI (expected on127.0.0.1:8080). If you don't run one, delete that block.
6a. Caddy on the host (server shared with other sites)
Install Caddy from its official packages (apt install caddy on Debian/Ubuntu after adding the repo). Then:
# 1. Global options + an import for per-site files, in /etc/caddy/Caddyfile (replacing the
# package's default welcome-page site):
# {
# servers {
# protocols h1 h2 # no HTTP/3: PocketBase's SSE stream is unreliable over QUIC
# }
# }
# import /etc/caddy/conf.d/*
sudo mkdir -p /etc/caddy/conf.d
# 2. RyftBorn's sites
sudo cp infra/caddy/ryftborn.caddy /etc/caddy/conf.d/
# 3. Tell Caddy the domain (and, optionally, who may reach the PocketBase admin UI)
sudo systemctl edit caddy
# [Service]
# Environment=RYFTBORN_DOMAIN=ryftborn.example.com
# Environment=PB_ADMIN_ALLOW_CIDR=127.0.0.1/32
# 4. Start the app stack (no Caddy in it), then load the config
podman compose build
podman compose up -d
sudo systemctl restart caddy
systemctl reload caddy validates a config before applying it and keeps the old one if it's broken, so a bad edit never takes the other sites down. The error server block without any key is global configuration means RYFTBORN_DOMAIN isn't set in Caddy's environment.
6b. Bundled Caddy (server runs only RyftBorn)
podman compose build
podman compose --profile edge up -d
The edge profile adds a Caddy container on the host network that imports the same caddy/ryftborn.caddy, with DOMAIN_FRONTEND from .env as the domain. Keep passing --profile edge to later up/down commands, or that container is left out.
Either way, the stack runs:
| Service | What it does |
|---|---|
| PocketBase | Auth, database, file storage |
| WS Server | Real-time chat, typing indicators, presence |
| LiveKit | Voice/video channels, screen sharing, embedded TURN for NAT |
| Web | The built frontend + landing page (static files, loopback only) |
| Caddy | Reverse proxy, automatic HTTPS certificates (host or bundled) |
Caddy automatically provisions TLS certificates once DNS resolves.
7. First-time app setup
- Open
https://app.ryftborn.example.com - Register the first account — you become the server owner
- The Setup Wizard walks you through naming your server, setting visibility, and creating your first channel
- Share the link with others so they can register and join
Architecture
Browser / Tauri App
│
├── HTTPS ──► Caddy (reverse proxy)
│ ├── /api/*, /_/* ──► PocketBase (auth, data, files)
│ ├── /ws ──► WS Server (real-time chat)
│ ├── /token ──► WS Server (LiveKit tokens)
│ ├── /preview ──► WS Server (link previews)
│ ├── /password-reset/* ──► WS Server (password reset)
│ ├── /admin/* ──► WS Server (sign-up + privileged writes)
│ ├── /registration/* ──► WS Server (join-request status)
│ ├── /rtc* ──► LiveKit (voice/video signaling)
│ └── /* ──► Web (frontend SPA, static)
│
└── UDP/TCP ──► LiveKit (WebRTC media on 50000-50200/udp + 7881/tcp,
embedded TURN on 3478/udp for NAT traversal)
In development (without Caddy), the app connects to each service directly via port:
- PocketBase:
http://localhost:8090 - WebSocket:
ws://localhost:7891 - LiveKit:
ws://localhost:7880
In production, everything goes through Caddy on a single domain with automatic TLS. Caddy runs outside the app stack (on the host, or the bundled container on the host network) and reaches every service on a loopback port.
Firewall Ports
| Port | Protocol | Service | When needed |
|---|---|---|---|
| 80 | TCP | Caddy (HTTP redirect) | Production |
| 443 | TCP | Caddy (HTTPS) | Production |
| 3478 | UDP | LiveKit embedded TURN | Production |
| 7881 | TCP | LiveKit RTC (TCP fallback) | Production |
| 50000-50200 | UDP | LiveKit RTC media | Production |
| 8090 | TCP | PocketBase | Dev only |
| 7891 | TCP | WebSocket server | Dev only |
| 7880 | TCP | LiveKit signaling | Dev only |
| 7892, 7893 | TCP | Web (app, landing page) | Loopback only — Caddy proxies to them |
In production, only ports 80, 443, 7881, 3478/udp, and 50000-50200/udp need to be open. Caddy handles all HTTP(S) routing; WebRTC media flows directly to LiveKit.
Common Operations
# View logs
podman compose logs -f # all services
podman compose logs -f pocketbase # single service
# Restart (add --profile edge to both if you use the bundled Caddy)
podman compose down && podman compose up -d
# Rebuild after code changes (the frontend lives in the web image, the real-time server in ws-server)
podman compose build web ws-server
podman compose up -d web ws-server
# After changing infra/caddy/ryftborn.caddy (host Caddy): copy it over and reload — reload keeps
# the running config if the new one doesn't validate. The bundled Caddy: restart its container.
sudo cp infra/caddy/ryftborn.caddy /etc/caddy/conf.d/ && sudo systemctl reload caddy
# PocketBase admin UI — NOT exposed publicly. Caddy answers 404 for /_/ unless your address is in
# PB_ADMIN_ALLOW_CIDR (see .env.example). Tunnel to the host's loopback bind instead:
# ssh -L 8090:127.0.0.1:8090 <server> then open http://localhost:8090/_/
# Dev: http://localhost:8090/_/ directly
# Back up data — PocketBase snapshots the database AND uploaded files into a zip every night at
# 03:00 (infra/pocketbase/pb_data/backups/, last 7 kept). Copy those zips somewhere off the box.
# Take one now, or restore one, from the admin UI → Settings → Backups.
# Do NOT `cp -r pb_data` of a running server: SQLite's write-ahead log makes a plain copy inconsistent.
ls infra/pocketbase/pb_data/backups/
# Reset everything (destroys all data)
podman compose down
rm -rf infra/pocketbase/pb_data
./setup.sh
Upgrading
cd RyftBorn && git pull
cd infra
podman compose build web ws-server # frontend + real-time server are built from source
podman compose up -d # PocketBase applies any new pb_migrations on start
# If the release touched infra/caddy/ryftborn.caddy and you run Caddy on the host:
sudo cp caddy/ryftborn.caddy /etc/caddy/conf.d/ && sudo systemctl reload caddy
Check podman compose logs pocketbase after the restart. A failed migration stops PocketBase; restore the newest zip from pb_data/backups/ (admin UI → Settings → Backups) and report the failing migration.
PocketBase and LiveKit are pinned with POCKETBASE_VERSION / LIVEKIT_VERSION in .env (see .env.example). Bump those deliberately, one at a time, with a fresh backup on hand — a latest tag can jump a major version underneath your database.
Desktop App (Tauri)
The desktop app wraps the same SvelteKit frontend in a native window.
bun install
bun run tauri dev
When connecting from the desktop app, enter your server address (e.g. https://app.ryftborn.example.com) in the connection dialog. The app automatically derives WebSocket and LiveKit URLs from it.
Releases
Pushing to main redeploys the server and cuts a desktop release. No version number is ever committed:
src-tauri/tauri.conf.jsonandsrc-tauri/Cargo.tomlstay at0.0.0in the repo. The git tag is the source of truth for a version.- CI reads the newest
v*tag, pushes the next patch tag, and the release workflow stamps that number into its own checkout at build time (.forgejo/stamp-version.mjs). Nothing is committed back tomain, so your clone never falls behind after a push andgit pullstops producing merge commits. - The build aborts if the stamped version does not appear in the installer's filename. An app whose version disagrees with the published update manifest makes every client reinstall the same build forever, so that mismatch must never ship.
Put [skip ci] in a commit message to redeploy the server without cutting a desktop release.
To rebuild a version that already exists, re-run the Release workflow against its tag.
Tech Stack
- Frontend: SvelteKit + Svelte 5 + TypeScript + Tailwind v4
- Desktop/Mobile: Tauri v2
- Backend: PocketBase (auth, CRUD, files, embedded SQLite)
- Real-time: Bun WebSocket server
- Voice/Video: LiveKit Server
- Reverse Proxy: Caddy (automatic TLS)
- NAT Traversal: LiveKit embedded TURN
- Icons: Lucide Svelte
- Package Manager: Bun
Project Structure
src/ # SvelteKit frontend (Svelte 5 + TypeScript + Tailwind v4)
src-tauri/ # Tauri v2 native shell
infra/ # Server infrastructure
docker-compose.yml # All services (LiveKit config generated inline from env vars)
Dockerfile.web # Multi-stage build: frontend → static file server
web/Caddyfile # Static serving inside the web container (plain HTTP)
caddy/ryftborn.caddy # Edge proxy sites: TLS + routing (host Caddy or bundled)
Caddyfile # Bundled edge Caddy (--profile edge) — imports caddy/ryftborn.caddy
ws-server/ # Bun WebSocket server
pocketbase/
pb_migrations/ # Database schema
pb_data/ # Database + file storage (created at runtime)
setup.sh # First-time setup script
.env # Secrets (generated by setup.sh, not committed)
landing/ # Static landing page served at root domain
License
MIT — see LICENSE.