- align auth docs with the simplified POST /auth/login flow - drop CODE_PEPPER / SMTP / MAIL_FROM / mail.js / request-code references - document the bifrost_joins table and bin/joins.js CLI - OPERATIONS.md: WSL setup, exclude data/.env/node_modules on promote rsync - INSTALL.md: 3-value /etc/fenja/env, drop SMTP prereq Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.2 KiB
Install
End-to-end setup for a fresh Ubuntu VPS running Nginx, Node, and SQLite. 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
- 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
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:
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:
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
Create a minimal placeholder Nginx block so certbot has something to attach to:
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):
rsync -avz --delete \
--exclude node_modules --exclude data --exclude .env --exclude .git \
./ user@vps:/tmp/fenja-upload/
On the VPS:
# NOTE: the --exclude list on the promote rsync must include `data`,
# `.env`, and `node_modules` so a later redeploy doesn't wipe them.
sudo rsync -a --delete \
--exclude data --exclude .env --exclude node_modules \
/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
cd /opt/fenja
sudo -u fenja npm ci --omit=dev
Should finish with added N packages, found 0 vulnerabilities.
5. Create the environment file
/etc/fenja/env is intentionally minimal — the server only reads three values (see .env.example). There are no secrets: auth is email-only against the invite list and the mail stack was removed.
sudo nano /etc/fenja/env
Paste:
PORT=3000
NODE_ENV=production
PUBLIC_ORIGIN=https://project-bifrost.fenja.ai
Lock permissions (matches PROJECT.md invariant — root:fenja, mode 640):
sudo chown root:fenja /etc/fenja/env
sudo chmod 640 /etc/fenja/env
6. Systemd unit
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:
sudo systemctl daemon-reload
sudo systemctl enable --now fenja
sudo journalctl -u fenja -n 20
Must show [bifrost] listening on 127.0.0.1:3000. (There is no [mail] SMTP relay reachable line — the mail stack was removed in the auth simplification.)
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:
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:
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:
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:
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:
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
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 -rwith no home: npm can't write its cache.mkdir /home/fenja && chown fenja:fenja.- CSP blocking inline
<script>: extract to a separate.jsfile and reference withsrc="..." 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.aistatically and shadowed the real one until removed.