Installer l'édition Node
L'édition Node tourne comme un service permanent : un processus, une base SQLite sur le disque, rien d'autre à installer — ni base de données externe, ni cache, ni file d'attente. Ce tutoriel va du dépôt vide à l'instance en production, y compris ce que la plupart des guides laissent de côté : le service système, le proxy inverse, le TLS, les sauvegardes et la mise à jour.
Ce qu'il faut#
| Élément | Version | Pourquoi |
|---|---|---|
| Node.js | 22 ou plus récent | Le produit s'appuie sur son lanceur de tests intégré et sur des API récentes. |
| Un compilateur C | build-essential | better-sqlite3 se compile à l'installation, sauf si un binaire précompilé existe pour votre plateforme. |
| Un disque local | — | SQLite et les fichiers déposés. Jamais un partage réseau : voir plus bas. |
Une machine à un cœur et 512 Mo de mémoire suffit pour quelques dizaines de personnes. Ce qui compte n'est pas la puissance mais le disque : il doit être local et sauvegardé.
1. Récupérer et installer#
git clone <votre-dépôt> /var/www/toutadmin
cd /var/www/toutadmin
npm ci --omit=dev
npm ci plutôt que npm install : il installe exactement ce que le
fichier de verrouillage décrit, sans jamais le réécrire. Sur un serveur, une version qui
glisse est une panne qu'on ne comprend pas.
better-sqlite3 est la seule dépendance qui compile du C.
sudo apt install -y build-essential python3 résout la quasi-totalité des cas.
2. Configurer#
cp .env.example .env
Tout se règle par variables d'environnement — lues depuis .env, ou posées par
votre gestionnaire de service. Aucune n'est obligatoire : sans .env, l'instance
démarre sur le port 3000 et vous envoie vers l'assistant.
| Variable | Défaut | Ce qu'elle fait |
|---|---|---|
PORT | 3000 | Le port d'écoute. |
NODE_ENV | — | production en production : cookies stricts, pas de traces détaillées. |
SESSION_SECRET | engendré | Scelle les sessions. Laissé vide, il est engendré dans data/session.key. |
INSTALL_TOKEN | — | Réclamé par l'assistant avant d'installer. Recommandé sur un serveur exposé. |
TRUST_PROXY | — | 1 derrière un proxy de confiance, et seulement là. |
DB_PATH | data/app.sqlite | La base. |
UPLOAD_DIR, CV_DIR, VAULT_DIR, SIGN_DIR, DOCS_DIR, BACKUP_DIR | sous data/ | Les dossiers de fichiers : photos, CV, coffre-fort, parapheur, pièces reçues, archives. |
LOGIN_RATE_LIMIT | 10 | Tentatives de connexion par quart d'heure et par adresse. |
GLOBAL_RATE_LIMIT | 300 | Requêtes par minute et par adresse. |
API_RATE_LIMIT | — | Appels d'API par minute et par jeton. |
SESSION_IDLE_MINUTES | 60 | Inactivité au-delà de laquelle la session tombe. |
SESSION_MAX_HOURS | 12 | Durée absolue qu'aucune activité ne repousse. |
ADMIN_EMAIL, ADMIN_PASSWORD | — | Installation sans interface : crée l'administrateur au démarrage. |
Changer SESSION_SECRET déconnecte tout le monde d'un coup. Posez-le une fois,
engendrez-le au hasard (node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"),
et sauvegardez-le avec le reste. Ne le laissez jamais dans un dépôt git.
Poser un jeton d'installation#
Entre le premier démarrage et votre passage dans l'assistant, l'instance est
à qui la trouve : le premier arrivé crée le compte d'administration.
Posez donc INSTALL_TOKEN avant d'ouvrir le port :
node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
# puis dans .env :
INSTALL_TOKEN=c3f1…
L'assistant le compare à temps constant — une comparaison ordinaire laisserait deviner le jeton caractère par caractère — et journalise chaque refus. Une fois l'instance installée, l'assistant se referme de lui-même.
Installer sans interface#
Pour un déploiement automatisé, ADMIN_EMAIL et ADMIN_PASSWORD
créent l'administrateur au démarrage, sans passer par l'assistant. Retirez-les ensuite :
un mot de passe dans l'environnement d'un service est lisible par qui lit ce service.
3. Permissions et emplacement des données#
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
Le code peut rester en lecture seule ; seul data/ doit être inscriptible.
SQLite écrit des fichiers voisins (-wal, -shm) : c'est le
dossier qui compte, pas seulement la base.
NFS et SMB mentent sur le verrouillage des fichiers. SQLite s'en remet à ce verrouillage pour empêcher deux écritures simultanées : sur un partage, la base finit par se corrompre sans prévenir. Disque local, toujours — et c'est la sauvegarde qui part ailleurs, pas la base.
4. Le service#
Lancé à la main, le produit s'arrête quand vous fermez le terminal. Confiez-le à 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
# Le service n'a besoin d'écrire que dans 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
Les cinq lignes de durcissement ne sont pas décoratives : ProtectSystem=strict
rend tout le système de fichiers non inscriptible, et ReadWritePaths rouvre
le seul dossier qui doit l'être. Une faille d'écriture arbitraire n'atteint alors que
data/.
5. Le proxy inverse#
Ne servez jamais le port 3000 directement sur Internet : il ne fait pas de TLS et n'a aucune raison d'apprendre à en faire.
server {
listen 443 ssl http2;
server_name intranet.exemple.fr;
ssl_certificate /etc/letsencrypt/live/intranet.exemple.fr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/intranet.exemple.fr/privkey.pem;
client_max_body_size 20M; # dépôts au coffre-fort, pièces reçues
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.exemple.fr;
return 301 https://$host$request_uri;
}
Faites aussi écouter le service sur la boucle locale seulement, pour que le port 3000 ne soit joignable que par le proxy.
TLS#
sudo certbot --nginx -d intranet.exemple.fr
Le produit pose l'en-tête Strict-Transport-Security dès que la connexion est
chiffrée, et jamais en clair : annoncé depuis une page non chiffrée il ne serait pas lu,
et il enfermerait un essai local en https pendant six mois.
TRUST_PROXY va avec le proxy, et seulement avec lui
Cette variable fait croire le serveur sur parole quant à l'adresse d'origine annoncée
dans un en-tête. Derrière nginx, c'est ce qu'il faut — sinon toutes les requêtes semblent
venir de 127.0.0.1 et les plafonds par adresse ne protègent plus rien. Sans
proxy devant, c'est l'inverse : n'importe qui annonce l'adresse de son choix et contourne
les plafonds.
6. L'assistant#
Ouvrez votre domaine. Tant qu'aucun compte n'existe, toute adresse renvoie vers
/installation. Cinq étapes :
- La langue de l'instance, parmi les 16.
- Les prérequis, vérifiés et affichés.
- L'organisation : le nom affiché partout.
- Les congés annuels attribués à chaque nouveau salarié non-freelance.
- Le compte d'administration : adresse et mot de passe d'au moins douze caractères.
Dès qu'un compte existe, /installation renvoie à la page de connexion :
l'assistant s'est refermé tout seul, il n'y a pas de fichier à supprimer à la main.
7. Le balayage périodique#
Contrairement à l'édition PHP, rien n'est à planifier : le serveur porte son propre ordonnanceur, qui se réveille toutes les heures et fait huit choses.
| Ce que fait le balayage | Garantie |
|---|---|
| Ferme les comptes dont le contrat est arrivé à terme | Vérifié aussi à la connexion et à l'ouverture du tableau de bord |
| Émet les factures des abonnements arrivés à échéance | Un index unique empêche de facturer deux fois |
| Transforme les échéances en notifications | Une clé de déduplication : un terme n'alerte qu'une fois |
| Purge les notifications lues et le journal d'audit | Selon la conservation choisie |
| Relève la boîte aux lettres comptable (IMAP) | Si la relève est configurée |
| Vide la file des webhooks, avec ses réessais | Cinq tentatives, puis abandon |
| Crée la sauvegarde automatique et l'externalise | Quand l'intervalle est écoulé |
Chaque opération est idempotente : un balayage rejoué ne facture pas deux fois et ne notifie pas deux fois. C'est ce qui permet de redémarrer le service à n'importe quel moment sans y réfléchir.
8. Vérifier que tout marche#
# les tests (ils n'écrivent pas dans votre base)
npm test
# une instance de démonstration, pour se promener
node scripts/seed-demo.js
Puis, dans l'interface : créez un membre, connectez-vous avec lui, déposez une pièce jointe, lancez une sauvegarde manuelle et vérifiez-la depuis l'écran Sauvegardes. Ces quatre gestes touchent la base, les fichiers, les droits et l'archive.
9. Mettre à jour#
# 1. une sauvegarde d'abord, toujours — depuis l'écran Sauvegardes
# 2. le code
cd /var/www/toutadmin
git pull
npm ci --omit=dev
# 3. redémarrer ; la base se met à jour toute seule au démarrage
sudo systemctl restart toutadmin
journalctl -u toutadmin -n 30 --no-pager
Le schéma évolue par migrations idempotentes : rejouer la mise à jour ne casse rien.
data/ et .env ne sont jamais touchés.
Revenir en arrière#
Repassez le code à la version précédente et redémarrez. Les migrations n'effacent pas de colonne : une base migrée reste lisible par la version d'avant, sauf mention explicite dans le journal des versions. En cas de doute, restaurez l'archive prise à l'étape 1.
10. Sauvegarder, vraiment#
L'archive tar.gz emporte la base et les cinq dossiers de fichiers. La base
est copiée par la sauvegarde en ligne de SQLite, qui produit une copie cohérente même
pendant une écriture — l'édition PHP obtient le même résultat par VACUUM INTO.
Chaque fichier porte son empreinte SHA-256, vérifiée à la restauration.
Une archive restée sur le serveur qu'elle protège ne protège de rien : configurez une
destination distante (FTPS ou Google Drive) depuis l'écran Sauvegardes, et
abonnez un webhook à sauvegarde.echec pour être prévenu quand l'externalisation
échoue. Le détail est sur la page Sauvegardes et restauration.
Essayer en local d'abord#
npm install
cp .env.example .env
npm run dev # redémarre à chaque modification
Ouvrez http://localhost:3000. En local, laissez NODE_ENV vide :
en production, les cookies sont marqués « sécurisés » et ne seront pas conservés
sur une connexion en clair — vous tourneriez en rond sur la page de connexion.
Passer à l'édition PHP, ou en venir#
Les deux éditions partagent le même schéma — 142 tables, 1311
colonnes — et le même format de mot de passe. Arrêtez l'une, copiez app.sqlite
et les dossiers de fichiers, démarrez l'autre : il n'y a pas de conversion.
Voir Les deux éditions.
Documentation de Toutadmin — construite le 2026-09-13. Site autonome, indépendant du logiciel.