# Install

End-to-end setup for a fresh Ubuntu VPS running Nginx, Node, SQLite, and an SMTP relay. Done once per server. Each block is ordered; don't skip.

Throughout: replace `project-bifrost.fenja.ai` with your actual domain and `user@vps` with your SSH user.

## 0. Prerequisites

- Ubuntu 22.04+ VPS with sudo
- Nginx installed and running
- An A record pointing the subdomain at the VPS's public IP
- SMTP relay credentials (STARTTLS on port 587) with SPF/DKIM/DMARC set on the sending domain
- Node 20+ installed (`curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash - && sudo apt install -y nodejs`)

## 1. Create the service user and directories

```bash
sudo useradd -r -s /usr/sbin/nologin fenja
sudo mkdir -p /home/fenja /opt/fenja /etc/fenja
sudo chown fenja:fenja /home/fenja /opt/fenja
```

## 2. DNS + TLS

Confirm the subdomain resolves to the VPS:

```bash
dig +short project-bifrost.fenja.ai
curl -s ifconfig.me
```

Both must match. If the domain is behind Cloudflare, set the DNS record to "DNS only" (grey cloud) for this subdomain — Let's Encrypt needs direct access.

Install certbot and issue the cert:

```bash
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
```

Create a minimal placeholder Nginx block so certbot has something to attach to:

```bash
sudo tee /etc/nginx/sites-available/project-bifrost <<'EOF'
server {
    listen 80;
    server_name project-bifrost.fenja.ai;
    root /var/www/html;
}
EOF
sudo ln -sf /etc/nginx/sites-available/project-bifrost /etc/nginx/sites-enabled/project-bifrost
sudo nginx -t && sudo systemctl reload nginx

sudo certbot --nginx -d project-bifrost.fenja.ai
```

Agree to ToS; say yes to the HTTP → HTTPS redirect.

## 3. Upload the code

On your laptop, from the project folder. Upload to staging, then move with sudo (your SSH user doesn't own `/opt/fenja`):

```bash
rsync -avz --delete \
  --exclude node_modules --exclude data --exclude .env --exclude .git \
  ./ user@vps:/tmp/fenja-upload/
```

On the VPS:

```bash
sudo rsync -a --delete /tmp/fenja-upload/ /opt/fenja/
sudo chown -R fenja:fenja /opt/fenja
sudo -u fenja mkdir -p /opt/fenja/data
sudo chmod 750 /opt/fenja/protected /opt/fenja/data
rm -rf /tmp/fenja-upload
```

## 4. Install dependencies

```bash
cd /opt/fenja
sudo -u fenja npm ci --omit=dev
```

Should finish with `added N packages, found 0 vulnerabilities`.

## 5. Create the environment file

```bash
sudo nano /etc/fenja/env
```

Paste (fill in real values):

```
PORT=3000
NODE_ENV=production
PUBLIC_ORIGIN=https://project-bifrost.fenja.ai

# Generate with: openssl rand -hex 32  (64 hex chars)
CODE_PEPPER=...

SMTP_HOST=smtp.yourrelay.tld
SMTP_PORT=587
SMTP_USER=...
SMTP_PASS=...
MAIL_FROM="Fenja AI <noreply@project-bifrost.fenja.ai>"
```

Lock permissions:

```bash
sudo chown root:fenja /etc/fenja/env
sudo chmod 640 /etc/fenja/env
```

## 6. Systemd unit

```bash
sudo cp /opt/fenja/deploy/fenja.service /etc/systemd/system/fenja.service
sudo nano /etc/systemd/system/fenja.service
```

Change `EnvironmentFile=/opt/fenja/.env` to:

```
EnvironmentFile=/etc/fenja/env
```

Enable and start:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now fenja
sudo journalctl -u fenja -n 20
```

Must show `[mail] SMTP relay reachable` and `[bifrost] listening on 127.0.0.1:3000`.

## 7. Nginx rate-limit zone

Add to `/etc/nginx/nginx.conf` inside the `http { ... }` block (anywhere near the top):

```
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=10r/m;
```

## 8. Replace the Nginx config

The placeholder from step 2 needs to be replaced with the real reverse-proxy config. First, check for conflicting older configs that might shadow the new one:

```bash
ls /etc/nginx/sites-enabled/
sudo grep -rln project-bifrost /etc/nginx/sites-available/
```

If you see any file other than `project-bifrost` (e.g. `project-bifrost.fenja.ai` or `default`), remove them:

```bash
sudo rm -f /etc/nginx/sites-enabled/default
sudo rm -f /etc/nginx/sites-enabled/project-bifrost.fenja.ai
sudo rm -f /etc/nginx/sites-available/project-bifrost.fenja.ai
```

Then install the real config:

```bash
sudo cp /opt/fenja/deploy/nginx.conf /etc/nginx/sites-available/project-bifrost
sudo ln -sf /etc/nginx/sites-available/project-bifrost /etc/nginx/sites-enabled/project-bifrost
sudo nginx -t
sudo systemctl reload nginx
```

## 9. Verify

From your laptop:

```bash
curl -I https://project-bifrost.fenja.ai/              # 200, with X-Frame-Options, CSP, etc.
curl -I https://project-bifrost.fenja.ai/timeline.js   # 302 → /
curl -I https://project-bifrost.fenja.ai/vendor/d3-array.min.js  # 302 → /
```

All three must behave as expected. The 200 response must include `X-Frame-Options`, `Content-Security-Policy`, and `Strict-Transport-Security` — their presence confirms Nginx is proxying to Node, not serving static files.

## 10. Ops hygiene

Nightly SQLite backup:

```bash
sudo tee /etc/cron.d/fenja-backup <<'EOF'
0 3 * * * fenja sqlite3 /opt/fenja/data/fenja.sqlite ".backup /opt/fenja/data/backup-$(date +\%F).sqlite"
0 4 * * * fenja find /opt/fenja/data -name 'backup-*.sqlite' -mtime +14 -delete
EOF
```

Uptime monitoring: set up an HTTP(S) check against `https://project-bifrost.fenja.ai/` at a 5-minute interval (UptimeRobot free tier works).

## 11. First invite

```bash
sudo -u fenja node /opt/fenja/bin/invite.js add you@yourdomain.com
```

Open `https://project-bifrost.fenja.ai/` in a fresh browser, walk the flow end-to-end.

---

## Gotchas we hit

- **Windows Node 24 + better-sqlite3 11.x**: no prebuilt binaries. Use `better-sqlite3 ^12.2.0`.
- **PowerShell blocks `npm`**: `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`.
- **`useradd -r` with no home**: npm can't write its cache. `mkdir /home/fenja && chown fenja:fenja`.
- **CSP blocking inline `<script>`**: extract to a separate `.js` file and reference with `src="..." defer`.
- **Two Nginx configs for the same domain**: whichever sorts first alphabetically in `sites-enabled/` wins. Keep only one.
- **Certbot placeholder config lingered**: the old one served `/var/www/project-bifrost.fenja.ai` statically and shadowed the real one until removed.
