# Self-Hosted Supabase Installation Guide This guide walks you through deploying Pulse with a fully self-hosted Supabase stack (PostgreSQL + GoTrue + Kong) using Docker Compose. No external Supabase project required. --- ## Table of Contents 1. [System Requirements](#system-requirements) 2. [Prerequisites](#prerequisites) 3. [Install Docker](#install-docker) 4. [Install Bun](#install-bun) 5. [Clone the Repository](#clone-the-repository) 6. [Generate Secrets](#generate-secrets) 7. [Configure Environment](#configure-environment) 8. [Build and Start](#build-and-start) 9. [Set Up HTTPS](#set-up-https) 10. [Firewall Configuration](#firewall-configuration) 11. [Claim Server Ownership](#claim-server-ownership) 12. [Verify Installation](#verify-installation) 13. [Updating Pulse](#updating-pulse) 14. [OAuth Setup (Optional)](#oauth-setup-optional) 15. [Federation Setup (Optional)](#federation-setup-optional) 16. [Troubleshooting](#troubleshooting) --- ## System Requirements | Resource | Minimum | Recommended | |----------|---------|-------------| | CPU | 2 cores | 4 cores | | RAM | 2 GB | 4 GB | | Disk | 10 GB | 20 GB+ (depends on file uploads) | | OS | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS | | Network | Public IP | Public IP + domain name | This deployment runs four Docker containers: PostgreSQL, GoTrue (auth), Kong (API gateway), and the Pulse application. --- ## Prerequisites - A server with Ubuntu 22.04+ and root/sudo access - A domain name with an A record pointing to your server's public IP (for HTTPS) - SSH access to your server - Git installed (`sudo apt install git` if not already present) --- ## Install Docker ```bash # Update system packages sudo apt update && sudo apt upgrade -y # Install required packages sudo apt install -y ca-certificates curl gnupg # Add Docker's official GPG key sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg # Add Docker repository echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # Install Docker Engine + Compose sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # Add your user to the docker group sudo usermod -aG docker $USER newgrp docker # Verify docker --version docker compose version ``` --- ## Install Bun Bun is needed to run the key generator script: ```bash curl -fsSL https://bun.sh/install | bash source ~/.bashrc bun --version ``` --- ## Clone the Repository ```bash sudo mkdir -p /opt/pulse sudo chown $USER:$USER /opt/pulse git clone https://github.com/plsechat/pulse-chat.git /opt/pulse cd /opt/pulse ``` --- ## Generate Secrets Generate a JWT secret and two Supabase API keys: ```bash cd /opt/pulse bun docker/generate-keys.ts ``` Output: ``` JWT_SECRET= SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_KEY= ``` **Copy all three values** for the next step. --- ## Configure Environment ```bash cp .env.supabase.example .env nano .env ``` Fill in the values: ```env # PostgreSQL password — strong and URL-safe (no / = + characters) POSTGRES_PASSWORD=ChangeMeToSomethingSecure123 # Paste the three values from generate-keys.ts JWT_SECRET= SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_KEY= # Your public domain (must match your DNS A record) SITE_URL=https://your-domain.com ``` > **Important:** > - `POSTGRES_PASSWORD` must be URL-safe. Avoid `/`, `=`, `+`, and other special characters. > - `SITE_URL` must start with `https://` for production. > - The `JWT_SECRET` generated by `generate-keys.ts` already meets the 32-character minimum. ### Environment Variable Reference | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `POSTGRES_PASSWORD` | Yes | — | Password for PostgreSQL | | `JWT_SECRET` | Yes | — | Secret key for signing JWTs (min 32 chars) | | `SUPABASE_ANON_KEY` | Yes | — | Supabase public/anonymous API key | | `SUPABASE_SERVICE_ROLE_KEY` | Yes | — | Supabase admin service role key | | `SITE_URL` | Yes | — | Public URL (e.g., `https://pulse.example.com`) | | `PULSE_PORT` | No | `4991` | Host port for Pulse | | `JWT_EXPIRY` | No | `3600` | Token expiry in seconds | | `PUBLIC_IP` | No | auto | Public IP for WebRTC | | `GOOGLE_OAUTH_ENABLED` | No | `false` | Enable Google login | | `GOOGLE_OAUTH_CLIENT_ID` | No | — | Google OAuth client ID | | `GOOGLE_OAUTH_SECRET` | No | — | Google OAuth client secret | | `DISCORD_OAUTH_ENABLED` | No | `false` | Enable Discord login | | `DISCORD_OAUTH_CLIENT_ID` | No | — | Discord OAuth client ID | | `DISCORD_OAUTH_SECRET` | No | — | Discord OAuth client secret | | `FACEBOOK_OAUTH_ENABLED` | No | `false` | Enable Facebook login | | `FACEBOOK_OAUTH_CLIENT_ID` | No | — | Facebook OAuth client ID | | `FACEBOOK_OAUTH_SECRET` | No | — | Facebook OAuth client secret | | `TWITCH_OAUTH_ENABLED` | No | `false` | Enable Twitch login | | `TWITCH_OAUTH_CLIENT_ID` | No | — | Twitch OAuth client ID | | `TWITCH_OAUTH_SECRET` | No | — | Twitch OAuth client secret | | `ADDITIONAL_REDIRECT_URLS` | No | — | Extra OAuth callback URLs | | `REGISTRATION_DISABLED` | No | `false` | Block new registrations (existing users can still log in; valid invite codes bypass) | | `GIPHY_API_KEY` | No | — | Giphy API key for GIF search | --- ## Build and Start ```bash cd /opt/pulse docker compose -f docker-compose-supabase.yml up -d ``` The first launch takes a few minutes while Docker downloads the required images. ### Verify containers are running ```bash docker compose -f docker-compose-supabase.yml ps ``` You should see four containers, all `Up`: ``` NAME IMAGE STATUS pulse-db supabase/postgres:15.6.1.143 Up (healthy) pulse-auth supabase/gotrue:v2.170.0 Up pulse-kong kong:3.4 Up pulse pulse-pulse Up ``` ### Check logs ```bash docker logs pulse ``` ### Test the health endpoint ```bash curl http://localhost:4991/healthz ``` Should return `{"status":"ok","timestamp":...}`. --- ## Set Up HTTPS Pulse does not handle TLS itself. Use a reverse proxy for HTTPS. ### Option A: Caddy (recommended) ```bash sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list sudo apt update sudo apt install caddy ``` Configure `/etc/caddy/Caddyfile`: ``` your-domain.com { handle /auth/v1/* { reverse_proxy localhost:8000 } handle { reverse_proxy localhost:4991 } } ``` ```bash sudo systemctl restart caddy sudo systemctl enable caddy ``` Caddy automatically obtains and renews Let's Encrypt certificates. ### Option B: Nginx ```bash sudo apt install nginx certbot python3-certbot-nginx ``` Create `/etc/nginx/sites-available/pulse`: ```nginx server { server_name your-domain.com; location /auth/v1/ { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location / { proxy_pass http://127.0.0.1:4991; 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-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 86400; } } ``` ```bash sudo ln -s /etc/nginx/sites-available/pulse /etc/nginx/sites-enabled/ sudo rm /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl restart nginx sudo certbot --nginx -d your-domain.com ``` --- ## Firewall Configuration ```bash sudo ufw allow 22/tcp # SSH sudo ufw allow 80/tcp # HTTP sudo ufw allow 443/tcp # HTTPS sudo ufw allow 40000:40020/udp # WebRTC voice/video sudo ufw allow 40000:40020/tcp # WebRTC TCP fallback sudo ufw enable ``` | Port | Protocol | Purpose | |------|----------|---------| | 22 | TCP | SSH access | | 80 | TCP | HTTP (redirects to HTTPS) | | 443 | TCP | HTTPS (web + WebSocket) | | 4991 | TCP | Pulse (only if no reverse proxy) | | 40000-40020 | UDP + TCP | WebRTC media (voice/video/screen share) | > **Note:** Docker manipulates iptables directly and can bypass ufw rules. Ports mapped in `docker-compose-supabase.yml` may be publicly accessible even if ufw doesn't allow them. The compose file binds Kong (port 8000) to `127.0.0.1` so it is only accessible from the reverse proxy — do not change this to `0.0.0.0`. --- ## Claim Server Ownership 1. Open `https://your-domain.com` in your browser 2. Register a new account 3. Find the ownership token in the server logs: ```bash docker logs pulse 2>&1 | grep -i token ``` 4. In the Pulse web interface, open browser DevTools (F12) 5. Go to the Console tab and run: `useToken('your_token_here')` 6. Your account is now the server owner > The ownership token is printed once on first start. Save it somewhere secure. --- ## Verify Installation - [ ] `https://your-domain.com` loads the login page - [ ] You can register and log in - [ ] You claimed ownership with the token - [ ] Text channels work (send/receive messages) - [ ] File uploads work - [ ] Voice channels work (join, speak, hear others) - [ ] `docker compose -f docker-compose-supabase.yml ps` shows all 4 containers healthy --- ## Updating Pulse ```bash cd /opt/pulse docker compose -f docker-compose-supabase.yml pull docker compose -f docker-compose-supabase.yml up -d ``` This pulls the latest published image and restarts the containers. Database migrations are handled automatically on startup. --- ## OAuth Setup (Optional) Pulse supports OAuth login via Google, Discord, Facebook, and Twitch through GoTrue. All OAuth configuration is done through your `.env` file — do not edit `docker-compose-supabase.yml` directly. ### Google OAuth 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 2. Create a project and go to **APIs & Services > Credentials** 3. Create an **OAuth 2.0 Client ID** (Web application) 4. Set the authorized redirect URI to: `https://your-domain.com/auth/v1/callback` 5. Add to your `.env`: ```env GOOGLE_OAUTH_ENABLED=true GOOGLE_OAUTH_CLIENT_ID=your-client-id GOOGLE_OAUTH_SECRET=your-client-secret ``` 6. Restart: `docker compose -f docker-compose-supabase.yml up -d` ### Discord OAuth 1. Go to [Discord Developer Portal](https://discord.com/developers/applications) 2. Create an application, go to **OAuth2**, add redirect: `https://your-domain.com/auth/v1/callback` 3. Add to your `.env`: ```env DISCORD_OAUTH_ENABLED=true DISCORD_OAUTH_CLIENT_ID=your-client-id DISCORD_OAUTH_SECRET=your-client-secret ``` 4. Restart: `docker compose -f docker-compose-supabase.yml up -d` The same pattern applies for Facebook and Twitch — replace `DISCORD` with `FACEBOOK` or `TWITCH` in the variable names. --- ## Federation Setup (Optional) Federation lets multiple Pulse instances connect so users can discover and join servers across instances. 1. Edit the config file: ```bash nano data/pulse/config.ini ``` 2. Add or modify: ```ini [federation] enabled=true domain=your-domain.com ``` 3. Restart: ```bash docker compose -f docker-compose-supabase.yml restart pulse ``` ### Connect Two Instances On Instance A: Go to **Server Settings > Federation** > **Generate Keys** > **Add Instance** (enter Instance B's domain). On Instance B: **Server Settings > Federation** > **Generate Keys** > Accept Instance A's request. --- ## Troubleshooting ### Containers won't start ```bash docker logs pulse-db docker logs pulse-auth docker logs pulse-kong docker logs pulse ``` Common issues: - **pulse-db**: `POSTGRES_PASSWORD` contains special characters. Use only alphanumeric characters. - **pulse-auth**: Database connection failed. Check `db` container is healthy: `docker compose -f docker-compose-supabase.yml ps` - **pulse-kong**: Config error. Verify `docker/kong-supabase.yml` is valid YAML. ### GoTrue auth fails ```bash docker exec pulse curl -s http://kong:8000/auth/v1/health ``` The `supabase_auth_admin` password is synced automatically on every container start via `docker/db-entrypoint.sh`. If you change `POSTGRES_PASSWORD` in your `.env`, just restart: ```bash docker compose -f docker-compose-supabase.yml up -d ``` The DB container will re-sync the password automatically. ### WebRTC voice/video not working 1. Check UDP ports are open: `sudo ufw status | grep 40000` 2. Check Docker port mapping: `docker port pulse` 3. If behind NAT, forward ports 40000-40020/UDP from your router 4. Check public IP detection: `docker logs pulse 2>&1 | grep -i "public ip"` ### Database issues ```bash docker exec -it pulse-db psql -U postgres \dt SELECT pg_size_pretty(pg_database_size('postgres')); ``` ### Reset everything ```bash cd /opt/pulse docker compose -f docker-compose-supabase.yml down -v rm -rf data/ docker compose -f docker-compose-supabase.yml up -d ``` ### View real-time logs ```bash docker compose -f docker-compose-supabase.yml logs -f docker compose -f docker-compose-supabase.yml logs -f pulse ```