Installing the Node edition
The Node edition runs as a permanent service: one process, a SQLite database on disk, nothing else to install — no external database, no cache, no queue. This tutorial goes from an empty directory to a production instance, including what most guides leave out: the system service, the reverse proxy, TLS, backups and upgrades.
What you need#
| Item | Version | Why |
|---|---|---|
| Node.js | 22 or newer | The product relies on its built-in test runner and on recent APIs. |
| A C compiler | build-essential | better-sqlite3 compiles at install time, unless a prebuilt binary exists for your platform. |
| A local disk | — | SQLite and the uploaded files. Never a network share: see below. |
A single-core machine with 512 MB of memory is enough for a few dozen people. What matters is not the power but the disk: it must be local and backed up.
1. Fetch and install#
git clone <your-repository> /var/www/toutadmin
cd /var/www/toutadmin
npm ci --omit=dev
npm ci rather than npm install: it installs exactly what the
lockfile describes, without ever rewriting it. On a server, a version that drifts is an
outage nobody can explain.
better-sqlite3 is the only dependency that compiles C.
sudo apt install -y build-essential python3 resolves almost every case.
2. Configure#
cp .env.example .env
Everything is set through environment variables — read from .env, or set by
your service manager. None of them is mandatory: without .env, the instance
starts on port 3000 and sends you to the wizard.
| Variable | Default | What it does |
|---|---|---|
PORT | 3000 | The listening port. |
NODE_ENV | — | production in production: strict cookies, no detailed traces. |
SESSION_SECRET | generated | Seals the sessions. Left empty, it is generated in data/session.key. |
INSTALL_TOKEN | — | Demanded by the wizard before installing. Recommended on an exposed server. |
TRUST_PROXY | — | 1 behind a trusted proxy, and only there. |
DB_PATH | data/app.sqlite | The database. |
UPLOAD_DIR, CV_DIR, VAULT_DIR, SIGN_DIR, DOCS_DIR, BACKUP_DIR | under data/ | The file folders: photos, CVs, vault, signature book, received documents, archives. |
LOGIN_RATE_LIMIT | 10 | Login attempts per quarter-hour and per address. |
GLOBAL_RATE_LIMIT | 300 | Requests per minute and per address. |
API_RATE_LIMIT | — | API calls per minute and per token. |
SESSION_IDLE_MINUTES | 60 | Inactivity beyond which the session drops. |
SESSION_MAX_HOURS | 12 | Absolute duration that no activity extends. |
ADMIN_EMAIL, ADMIN_PASSWORD | — | Headless install: creates the administrator at startup. |
Changing SESSION_SECRET logs everybody out at once. Set it once, generate it
at random
(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"),
and back it up with the rest. Never leave it in a git repository.
Setting an install token#
Between the first start and your visit to the wizard, the instance belongs to
whoever finds it: the first to arrive creates the administration account.
So set INSTALL_TOKEN before opening the port:
node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
# then in .env:
INSTALL_TOKEN=c3f1…
The wizard compares it in constant time — an ordinary comparison would let the token be guessed character by character — and logs every refusal. Once the instance is installed, the wizard closes by itself.
Installing headless#
For an automated deployment, ADMIN_EMAIL and ADMIN_PASSWORD
create the administrator at startup, without going through the wizard. Remove them
afterwards: a password in a service's environment is readable by whoever reads that service.
3. Permissions and where the data lives#
sudo useradd --system --home /var/www/toutadmin --shell /usr/sbin/nologin toutadmin
sudo chown -R toutadmin:toutadmin /var/www/toutadmin/data
sudo chmod 750 /var/www/toutadmin/data
sudo chmod 640 /var/www/toutadmin/.env
The code can stay read-only; only data/ must be writable. SQLite writes
sibling files (-wal, -shm): it is the folder that
counts, not just the database.
NFS and SMB lie about file locking. SQLite relies on that locking to stop two simultaneous writes: on a share, the database ends up corrupted without warning. A local disk, always — and it is the backup that goes elsewhere, not the database.
4. The service#
Started by hand, the product stops when you close the terminal. Hand it to systemd:
# /etc/systemd/system/toutadmin.service
[Unit]
Description=Toutadmin
After=network.target
[Service]
Type=simple
User=toutadmin
WorkingDirectory=/var/www/toutadmin
EnvironmentFile=/var/www/toutadmin/.env
ExecStart=/usr/bin/node src/server.js
Restart=always
RestartSec=5
# The service only needs to write inside data/.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/toutadmin/data
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now toutadmin
sudo systemctl status toutadmin
journalctl -u toutadmin -f
The five hardening lines are not decoration: ProtectSystem=strict makes the
whole filesystem read-only, and ReadWritePaths reopens the single folder that
must not be. An arbitrary-write flaw then reaches nothing but data/.
5. The reverse proxy#
Never serve port 3000 straight onto the internet: it does no TLS, and has no reason to learn how.
server {
listen 443 ssl http2;
server_name intranet.example.com;
ssl_certificate /etc/letsencrypt/live/intranet.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/intranet.example.com/privkey.pem;
client_max_body_size 20M; # vault uploads, received documents
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
server {
listen 80;
server_name intranet.example.com;
return 301 https://$host$request_uri;
}
Also make the service listen on the loopback only, so port 3000 can be reached by the proxy alone.
TLS#
sudo certbot --nginx -d intranet.example.com
The product sets the Strict-Transport-Security header as soon as the
connection is encrypted, and never in the clear: announced from an unencrypted page it
would not be read, and it would lock a local trial into https for six months.
TRUST_PROXY goes with the proxy, and only with it
This variable makes the server take on trust the origin address announced in a header.
Behind nginx, that is what you want — otherwise every request appears to come from
127.0.0.1 and the per-address limits protect nothing. With no proxy in
front, it is the opposite: anyone announces the address of their choice and walks past
the limits.
6. The wizard#
Open your domain. As long as no account exists, every address redirects to
/installation. Five steps:
- The language of the instance, among the 16.
- The prerequisites, checked and displayed.
- The organisation: the name shown everywhere.
- The annual leave granted to every new non-freelance employee.
- The administration account: address and a password of at least twelve characters.
As soon as an account exists, /installation redirects to the login page: the
wizard has closed itself, there is no file to delete by hand.
7. The periodic sweep#
Unlike the PHP edition, there is nothing to schedule: the server carries its own scheduler, which wakes every hour and does eight things.
| What the sweep does | Guarantee |
|---|---|
| Closes the accounts whose contract has run out | Also checked at login and when the dashboard opens |
| Issues the invoices of subscriptions that have come due | A unique index prevents invoicing twice |
| Turns deadlines into notifications | A deduplication key: one deadline alerts once |
| Purges read notifications and the audit log | According to the chosen retention |
| Collects the accounting mailbox (IMAP) | If collection is configured |
| Drains the webhook queue, with its retries | Five attempts, then abandon |
| Creates the automatic backup and sends it off-site | When the interval has elapsed |
Every operation is idempotent: a replayed sweep does not invoice twice and does not notify twice. That is what makes it safe to restart the service at any moment without thinking about it.
8. Check that it all works#
# the tests (they do not write into your database)
npm test
# a demonstration instance, to walk around in
node scripts/seed-demo.js
Then, in the interface: create a member, log in as them, upload an attachment, run a manual backup and verify it from the Backups screen. Those four gestures touch the database, the files, the rights and the archive.
9. Upgrading#
# 1. a backup first, always — from the Backups screen
# 2. the code
cd /var/www/toutadmin
git pull
npm ci --omit=dev
# 3. restart; the database migrates itself at startup
sudo systemctl restart toutadmin
journalctl -u toutadmin -n 30 --no-pager
The schema evolves through idempotent migrations: replaying the upgrade breaks nothing.
data/ and .env are never touched.
Rolling back#
Put the code back on the previous version and restart. Migrations never drop a column: a migrated database stays readable by the previous version, unless the changelog says otherwise. When in doubt, restore the archive taken at step 1.
10. Backing up, properly#
The tar.gz archive carries the database and the five file folders. The
database is copied through SQLite's online backup, which produces a consistent copy even
during a write — the PHP edition gets the same result through VACUUM INTO.
Every file carries its SHA-256 hash, verified on restore.
An archive that stays on the server it protects protects nothing: configure a remote
destination (FTPS or Google Drive) from the Backups screen, and subscribe a
webhook to sauvegarde.echec to be told when sending off-site fails. The detail
is on the Backups and restore page.
Try it locally first#
npm install
cp .env.example .env
npm run dev # restarts on every change
Open http://localhost:3000. Locally, leave NODE_ENV empty: in
production, cookies are marked "secure" and will not be kept over an
unencrypted connection — you would go round in circles on the login page.
Moving to the PHP edition, or coming from it#
Both editions share the same schema — 142 tables, 1311 columns —
and the same password format. Stop one, copy app.sqlite and the file folders,
start the other: there is no conversion. See The two editions.
Toutadmin documentation — built on 2026-09-13. A standalone site, independent of the software.