Instalar la edición Node
La edición Node funciona como un servicio permanente: un proceso, una base SQLite en el disco, nada más que instalar —ni base de datos externa, ni caché, ni cola—. Este tutorial va del directorio vacío a la instancia en producción, incluido lo que la mayoría de las guías deja de lado: el servicio del sistema, el proxy inverso, el TLS, las copias de seguridad y la actualización.
Lo que hace falta#
| Elemento | Versión | Por qué |
|---|---|---|
| Node.js | 22 o más reciente | El producto se apoya en su lanzador de pruebas integrado y en API recientes. |
| Un compilador C | build-essential | better-sqlite3 se compila al instalarse, salvo que exista un binario precompilado para su plataforma. |
| Un disco local | — | SQLite y los archivos depositados. Nunca un recurso compartido de red: véase más abajo. |
Una máquina de un núcleo y 512 MB de memoria basta para unas decenas de personas. Lo que cuenta no es la potencia sino el disco: debe ser local y estar respaldado.
1. Obtener e instalar#
git clone <su-repositorio> /var/www/toutadmin
cd /var/www/toutadmin
npm ci --omit=dev
npm ci en lugar de npm install: instala exactamente lo que
describe el archivo de bloqueo, sin reescribirlo nunca. En un servidor, una versión que se
desplaza es una avería que nadie entiende.
better-sqlite3 es la única dependencia que compila C.
sudo apt install -y build-essential python3 resuelve la práctica totalidad
de los casos.
2. Configurar#
cp .env.example .env
Todo se ajusta con variables de entorno, leídas desde .env o puestas por su
gestor de servicios. Ninguna es obligatoria: sin .env, la instancia arranca en
el puerto 3000 y le envía al asistente.
| Variable | Por defecto | Qué hace |
|---|---|---|
PORT | 3000 | El puerto de escucha. |
NODE_ENV | — | production en producción: cookies estrictas, sin trazas detalladas. |
SESSION_SECRET | generado | Sella las sesiones. Si se deja vacío, se genera en data/session.key. |
INSTALL_TOKEN | — | Lo reclama el asistente antes de instalar. Recomendado en un servidor expuesto. |
TRUST_PROXY | — | 1 detrás de un proxy de confianza, y solo ahí. |
DB_PATH | data/app.sqlite | La base. |
UPLOAD_DIR, CV_DIR, VAULT_DIR, SIGN_DIR, DOCS_DIR, BACKUP_DIR | bajo data/ | Las carpetas de archivos: fotos, CV, caja fuerte, portafirmas, documentos recibidos, archivos. |
LOGIN_RATE_LIMIT | 10 | Intentos de acceso por cuarto de hora y por dirección. |
GLOBAL_RATE_LIMIT | 300 | Peticiones por minuto y por dirección. |
API_RATE_LIMIT | — | Llamadas de API por minuto y por token. |
SESSION_IDLE_MINUTES | 60 | Inactividad tras la cual cae la sesión. |
SESSION_MAX_HOURS | 12 | Duración absoluta que ninguna actividad prolonga. |
ADMIN_EMAIL, ADMIN_PASSWORD | — | Instalación sin interfaz: crea el administrador al arrancar. |
Cambiar SESSION_SECRET desconecta a todo el mundo de golpe. Póngalo una vez,
genérelo al azar
(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"), y
guárdelo con el resto. No lo deje nunca en un repositorio git.
Poner un token de instalación#
Entre el primer arranque y su paso por el asistente, la instancia es de quien la
encuentre: el primero en llegar crea la cuenta de administración. Ponga, pues,
INSTALL_TOKEN antes de abrir el puerto:
node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
# luego en .env:
INSTALL_TOKEN=c3f1…
El asistente lo compara en tiempo constante —una comparación ordinaria dejaría adivinar el token carácter a carácter— y registra cada rechazo. Una vez instalada la instancia, el asistente se cierra solo.
Instalar sin interfaz#
Para un despliegue automatizado, ADMIN_EMAIL y ADMIN_PASSWORD
crean el administrador al arrancar, sin pasar por el asistente. Retírelos después: una
contraseña en el entorno de un servicio la lee quien lea ese servicio.
3. Permisos y dónde viven los datos#
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
El código puede quedar en solo lectura; solo data/ debe ser escribible. SQLite
escribe archivos vecinos (-wal, -shm): lo que cuenta es la
carpeta, no solo la base.
NFS y SMB mienten sobre el bloqueo de archivos. SQLite se apoya en ese bloqueo para impedir dos escrituras simultáneas: en un recurso compartido, la base acaba corrompiéndose sin avisar. Disco local, siempre —y es la copia de seguridad la que se va a otra parte, no la base—.
4. El servicio#
Lanzado a mano, el producto se detiene al cerrar la terminal. Confíelo a 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
# El servicio solo necesita escribir en 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
Las cinco líneas de endurecimiento no son decorativas:
ProtectSystem=strict deja todo el sistema de archivos no escribible, y
ReadWritePaths reabre la única carpeta que debe serlo. Un fallo de escritura
arbitraria alcanza entonces solo a data/.
5. El proxy inverso#
No sirva nunca el puerto 3000 directamente en internet: no hace TLS y no tiene por qué aprender a hacerlo.
server {
listen 443 ssl http2;
server_name intranet.ejemplo.com;
ssl_certificate /etc/letsencrypt/live/intranet.ejemplo.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/intranet.ejemplo.com/privkey.pem;
client_max_body_size 20M; # depósitos en la caja fuerte, documentos recibidos
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.ejemplo.com;
return 301 https://$host$request_uri;
}
Haga además que el servicio escuche solo en el bucle local, para que el puerto 3000 sea alcanzable únicamente por el proxy.
TLS#
sudo certbot --nginx -d intranet.ejemplo.com
El producto pone la cabecera Strict-Transport-Security en cuanto la conexión
está cifrada, y nunca en claro: anunciada desde una página sin cifrar no se leería, y
encerraría una prueba local en https durante seis meses.
TRUST_PROXY va con el proxy, y solo con él
Esta variable hace que el servidor se crea la dirección de origen anunciada en una
cabecera. Detrás de nginx es lo que hace falta: si no, todas las peticiones parecen venir
de 127.0.0.1 y los límites por dirección ya no protegen nada. Sin proxy
delante es al revés: cualquiera anuncia la dirección que quiera y esquiva los límites.
6. El asistente#
Abra su dominio. Mientras no exista ninguna cuenta, toda dirección redirige a
/installation. Cinco pasos:
- El idioma de la instancia, entre los 16.
- Los requisitos, comprobados y mostrados.
- La organización: el nombre que se muestra en todas partes.
- Las vacaciones anuales atribuidas a cada nuevo empleado no autónomo.
- La cuenta de administración: dirección y contraseña de al menos doce caracteres.
En cuanto existe una cuenta, /installation redirige a la página de acceso: el
asistente se ha cerrado solo, no hay ningún archivo que borrar a mano.
7. El barrido periódico#
A diferencia de la edición PHP, no hay nada que programar: el servidor lleva su propio planificador, que se despierta cada hora y hace ocho cosas.
| Qué hace el barrido | Garantía |
|---|---|
| Cierra las cuentas cuyo contrato ha vencido | Comprobado también al entrar y al abrir el panel |
| Emite las facturas de las suscripciones vencidas | Un índice único impide facturar dos veces |
| Convierte los vencimientos en notificaciones | Una clave de deduplicación: un vencimiento avisa una vez |
| Purga las notificaciones leídas y el registro de auditoría | Según la conservación elegida |
| Recoge el buzón contable (IMAP) | Si la recogida está configurada |
| Vacía la cola de webhooks, con sus reintentos | Cinco intentos, luego abandono |
| Crea la copia automática y la externaliza | Cuando ha transcurrido el intervalo |
Cada operación es idempotente: un barrido repetido no factura dos veces ni notifica dos veces. Es lo que permite reiniciar el servicio en cualquier momento sin pensarlo.
8. Comprobar que todo funciona#
# las pruebas (no escriben en su base)
npm test
# una instancia de demostración, para dar una vuelta
node scripts/seed-demo.js
Luego, en la interfaz: cree un miembro, entre con él, deposite un adjunto, lance una copia manual y verifíquela desde la pantalla Copias de seguridad. Esos cuatro gestos tocan la base, los archivos, los permisos y el archivo comprimido.
9. Actualizar#
# 1. una copia de seguridad primero, siempre — desde la pantalla Copias de seguridad
# 2. el código
cd /var/www/toutadmin
git pull
npm ci --omit=dev
# 3. reiniciar; la base se actualiza sola al arrancar
sudo systemctl restart toutadmin
journalctl -u toutadmin -n 30 --no-pager
El esquema evoluciona por migraciones idempotentes: repetir la actualización no rompe nada.
data/ y .env no se tocan nunca.
Volver atrás#
Devuelva el código a la versión anterior y reinicie. Las migraciones no borran columnas: una base migrada sigue siendo legible por la versión anterior, salvo mención explícita en el registro de versiones. En caso de duda, restaure el archivo tomado en el paso 1.
10. Hacer copias de verdad#
El archivo tar.gz lleva la base y las cinco carpetas de archivos. La base se
copia mediante la copia en línea de SQLite, que produce una copia coherente incluso durante
una escritura —la edición PHP obtiene el mismo resultado mediante VACUUM INTO—.
Cada archivo lleva su huella SHA-256, verificada al restaurar.
Un archivo que se queda en el servidor que protege no protege nada: configure un destino
remoto (FTPS o Google Drive) desde la pantalla Copias de seguridad, y suscriba un
webhook a sauvegarde.echec para saber cuándo falla la externalización. El
detalle está en la página Copias de seguridad y restauración.
Probarlo primero en local#
npm install
cp .env.example .env
npm run dev # reinicia a cada modificación
Abra http://localhost:3000. En local, deje NODE_ENV vacío: en
production, las cookies se marcan como «seguras» y no se conservarán en una
conexión en claro; daría vueltas en la página de acceso.
Pasar a la edición PHP, o venir de ella#
Las dos ediciones comparten el mismo esquema —142 tablas,
1311 columnas— y el mismo formato de contraseña. Detenga una, copie
app.sqlite y las carpetas de archivos, arranque la otra: no hay conversión.
Véase Las dos ediciones.
Documentación de Toutadmin, construida el 2026-09-13. Sitio autónomo, independiente del software.