Self-Hosting Guide
Don't want to self-host? A managed hosted service is coming soon at nectenda.com.
Docker Deployment
Basic Setup
docker run -d \
--name nectenda \
-p 1234:1234 \
-v nectenda-data:/app/data \
-e JWT_SECRET="$(openssl rand -hex 32)" \
-e ADMIN_USERNAME="admin" \
-e ADMIN_PASSWORD="choose-a-strong-password" \
nectenda/nectenda
Docker Compose
services:
nectenda:
image: nectenda/nectenda
container_name: nectenda
restart: unless-stopped
ports:
- "1234:1234"
volumes:
- nectenda-data:/app/data
environment:
- JWT_SECRET=your-secret-key-here
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=your-admin-password
- LOG_LEVEL=info
volumes:
nectenda-data:
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SECRET |
Yes | insecure default | Secret for JWT signing. Generate with openssl rand -hex 32. The server warns on startup if not set. |
ADMIN_USERNAME |
No | — | Creates an admin user on first run. Only runs if the user doesn't already exist. |
ADMIN_PASSWORD |
No | — | Password for the auto-created admin user. |
PORT |
No | 1234 |
HTTP/WebSocket listen port. |
SQLITE_PATH |
No | /app/data/docs.db |
Path to the SQLite database file. Must be on the persistent volume. |
LOG_LEVEL |
No | info |
Minimum log level: debug, info, warn, error. |
Reverse Proxy
Nectenda uses WebSockets for real-time sync. Your reverse proxy must support WebSocket upgrade.
Nginx
server {
listen 443 ssl;
server_name sync.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:1234;
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;
}
}
Caddy
sync.example.com {
reverse_proxy localhost:1234
}
Caddy handles WebSocket upgrade, TLS, and headers automatically.
Plugin Configuration
Once the server is running:
- In Obsidian, go to Settings → Nectenda
- Set the server URL:
- Local:
ws://localhost:1234 - With TLS:
wss://sync.example.com
- Local:
- Log in with the admin credentials
- Generate invite tokens for other users (Admin panel → Generate Invite)
User Management
- Admin creates invite tokens via the plugin's Admin panel
- Users register with an invite token (one-time use)
- Admin can remove users via the Admin panel
- Deleted users' active sessions expire when their JWT expires (7 days max) or on their next WebSocket reconnection
Backup
The server stores all data in a single SQLite database:
Document content: an append-only log of encrypted Yjs updates in
doc_updates, compacted intodoc_snapshots. The server has no key for any of it, and the document names are HMACs rather than paths, so a backup of this database reveals neither note content nor folder structure. Attachments are stored as sealed blobs underDATA_DIRwith their metadata inblobs.This means your backups are only as recoverable as your users' passwords. Keys are derived on the client; restoring the database does not restore the ability to read it. See
docs/security-model.md.Users, invites, shared folders: stored in the same database via better-sqlite3
Backup strategy
# Hot backup (SQLite WAL mode is safe for concurrent reads)
docker exec nectenda cp /app/data/docs.db /app/data/docs.db.bak
# Or from the host, copy the volume
docker cp nectenda:/app/data/docs.db ./backup-$(date +%Y%m%d).db
For automated backups, use a cron job or your orchestrator's backup mechanism on the Docker volume.
Restore
docker stop nectenda
docker cp ./backup.db nectenda:/app/data/docs.db
docker start nectenda
Updating
docker pull nectenda/nectenda
docker stop nectenda
docker rm nectenda
# Re-run docker run with the same volume and env vars
Or with Docker Compose:
docker compose pull
docker compose up -d
Data persists across updates via the Docker volume.
Migrating from Hyperfector
The project was renamed from Hyperfector to Nectenda. The Docker volume was renamed with
it, from hyperfector-data to nectenda-data. Docker creates the new volume empty, so a
server started after the upgrade will not see your existing database until you copy it
across:
# Stop the old container first
docker stop hyperfector
# Copy docs.db out of the old volume
docker run --rm -v hyperfector-data:/old -v "$PWD":/backup alpine \
cp /old/docs.db /backup/docs.db
# Start the new container (creates nectenda-data), then copy the database in
docker compose up -d
docker stop nectenda
docker cp ./docs.db nectenda:/app/data/docs.db
docker start nectenda
Once you have confirmed the new container comes up with your data intact, remove the old
volume with docker volume rm hyperfector-data.
The Obsidian plugin id changed too, from hyperfector to nectenda. Each user must delete
.obsidian/plugins/hyperfector/ from their vault and install the plugin into
.obsidian/plugins/nectenda/. Obsidian keys plugin settings by id, so server URL and login
details will need to be entered again.
Security Notes
- The server runs as a non-root user inside the container
- Auth endpoints are rate-limited (10 login attempts / 15 min per IP, 5 registrations / 15 min per IP)
- Request body size is limited to 1 MB
- Input validation enforces username format (alphanumeric + underscore, 1-32 chars) and minimum password length (8 chars)
- Always set
JWT_SECRETin production — the default is insecure and logs a warning - Use TLS (wss://) in production — credentials are sent over WebSocket