T ToutadminDocumentation

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#

ItemVersionWhy
Node.js22 or newerThe product relies on its built-in test runner and on recent APIs.
A C compilerbuild-essentialbetter-sqlite3 compiles at install time, unless a prebuilt binary exists for your platform.
A local diskSQLite 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.

If the build fails

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.

VariableDefaultWhat it does
PORT3000The listening port.
NODE_ENVproduction in production: strict cookies, no detailed traces.
SESSION_SECRETgeneratedSeals the sessions. Left empty, it is generated in data/session.key.
INSTALL_TOKENDemanded by the wizard before installing. Recommended on an exposed server.
TRUST_PROXY1 behind a trusted proxy, and only there.
DB_PATHdata/app.sqliteThe database.
UPLOAD_DIR, CV_DIR, VAULT_DIR, SIGN_DIR, DOCS_DIR, BACKUP_DIRunder data/The file folders: photos, CVs, vault, signature book, received documents, archives.
LOGIN_RATE_LIMIT10Login attempts per quarter-hour and per address.
GLOBAL_RATE_LIMIT300Requests per minute and per address.
API_RATE_LIMITAPI calls per minute and per token.
SESSION_IDLE_MINUTES60Inactivity beyond which the session drops.
SESSION_MAX_HOURS12Absolute duration that no activity extends.
ADMIN_EMAIL, ADMIN_PASSWORDHeadless install: creates the administrator at startup.
The session secret

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.

Never on a network share

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:

  1. The language of the instance, among the 16.
  2. The prerequisites, checked and displayed.
  3. The organisation: the name shown everywhere.
  4. The annual leave granted to every new non-freelance employee.
  5. 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 doesGuarantee
Closes the accounts whose contract has run outAlso checked at login and when the dashboard opens
Issues the invoices of subscriptions that have come dueA unique index prevents invoicing twice
Turns deadlines into notificationsA deduplication key: one deadline alerts once
Purges read notifications and the audit logAccording to the chosen retention
Collects the accounting mailbox (IMAP)If collection is configured
Drains the webhook queue, with its retriesFive attempts, then abandon
Creates the automatic backup and sends it off-siteWhen 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.