# 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=<base64 string>
SUPABASE_ANON_KEY=<jwt token>
SUPABASE_SERVICE_ROLE_KEY=<jwt token>
```

**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=<paste here>
SUPABASE_ANON_KEY=<paste here>
SUPABASE_SERVICE_ROLE_KEY=<paste here>

# 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
```
