El mòdul 10 va acabar amb una constatació incòmoda: Escena Viva funciona. Reparteix càrrega entre treballadors, guarda el catàleg a la memòria cau de Redis, processa les cues d'emissió d'entrades en un procés a part, mesura el seu propi percentil 99 i exposa una API REST i una altra de GraphQL. I tot això continua executant-se en un portàtil, amb els secrets en un fitxer .env que només existeix al teu disc, sense registres centralitzats, sense supervisor, sense contenidor i sense desplegament automàtic. Aquest mòdul tanca aquesta distància. I comença per on cal començar: per la configuració. No perquè sigui el més vistós, sinó perquè és el primer que es trenca quan una aplicació surt del portàtil. La mateixa base de codi ha d'arrencar a la teva màquina apuntant a una base de dades local, al servidor d'integració contínua apuntant a contenidors efímers, en preproducció apuntant a una còpia de les dades reals i en producció apuntant a la base de dades on la Lucía i en Marc compren de veritat les seves entrades per al Festival de Jazz de Primavera. Una sola base de codi, quatre comportaments. La diferència està sencera a la configuració.

Contingut

  1. Els dotze factors aplicats a la configuració
  2. process.env de veritat
  3. dotenv i els seus límits
  4. Validar en arrencar i fallar de pressa
  5. Configuració per entorn sense duplicar fitxers
  6. Què no va mai en una variable d'entorn
  7. Gestió de secrets de debò
  8. Rotació de secrets sense parada
  9. Quan un secret es filtra
  10. Configuració en calent enfront de reinici
  11. Taula de referència de les variables d'Escena Viva

  1. Els dotze factors aplicats a la configuració

The Twelve-Factor App és un manifest del 2011 que descriu com s'hauria de construir una aplicació que es desplega en serveis al núvol. El seu factor III diu, en una frase: desa la configuració a l'entorn. La idea de fons és separar allò que canvia entre desplegaments d'allò que no. El codi d'Escena Viva és idèntic al teu portàtil i en producció: el mateix crearAplicacio, els mateixos repositoris, el mateix càlcul d'aforament. El que canvia és a quina base de dades es connecta, amb quina clau signa els JWT, en quin port escolta i quants treballadors arrenca. Això és configuració. La prova pràctica per distingir una cosa de l'altra és la que els dotze factors anomenen la prova del repositori públic:

¿Podries fer públic el repositori d'Escena Viva ara mateix, sense canviar res, sense comprometre cap credencial?

Si la resposta és no, tens configuració dins del codi. Tant se val si és en un config.json, en un objecte JavaScript o en un comentari: si està versionat i és secret, has suspès la prova. Compte amb el matís: la prova parla de credencials, no de qualsevol valor que variï. L'aforament màxim de la Sala Bóveda no és configuració per entorn: és una regla de negoci, i viu al domini o a la base de dades. Un error molt habitual és convertir en variable d'entorn tot allò que en algun moment podria canviar, i acabar amb quaranta variables que ningú sap per a què serveixen.

És configuració No és configuració
URL de PostgreSQL, MongoDB, Redis Nom de les taules i col·leccions
JWT_SECRET, claus d'API de tercers Durada de negoci d'un token de refresc decidida per producte
Port d'escolta, nombre de treballadors Regles d'aforament, preus base, tipus de sala
Origen permès per CORS Capçaleres de seguretat que sempre són les mateixes
Nivell de registre Format de l'identificador d'entrada EV-<any>-<6 dígits>

La regla mental: si dos desplegaments del mateix codi necessiten valors diferents, és configuració. Si tots els desplegaments necessiten el mateix valor, és codi.

  1. process.env de veritat

Node exposa les variables d'entorn del procés a process.env. És un objecte normal, però té tres característiques que causen la majoria dels errors. Tot són cadenes. Sempre. No hi ha nombres, ni booleans, ni nuls.

// Suposant: PORT=3000 DEPURAR=false TREBALLADORS=0
typeof process.env.PORT;                      // 'string'
process.env.PORT + 1;                         // '30001'  (concatenacio, no suma)
Boolean(process.env.DEPURAR);                 // true  (la cadena 'false' es certa)
Number(process.env.TREBALLADORS) || 4;        // 4  (el 0 es perd)

Les tres línies són errors reals que es veuen en producció. L'última és especialment traïdora: || 4 sembla un valor per defecte raonable fins que algú demana explícitament zero treballadors i n'obté quatre. Per això més endavant convertirem tipus amb un esquema, no a mà. undefined no és el mateix que cadena buida. Una variable que no existeix dóna undefined; una variable declarada sense valor dóna ''. Totes dues són falses en un if, però signifiquen coses diferents: la primera és «no m'ho has dit», la segona és «et dic explícitament que està buit».

node -e "console.log(process.env.ORIGEN_CORS)"                              # undefined
ORIGEN_CORS= node -e "console.log(JSON.stringify(process.env.ORIGEN_CORS))" # ""

Majúscules per convenció. No és una regla de Node, és una convenció d'Unix heretada: les variables d'entorn van en MAJUSCULES_AMB_GUIO_BAIX. Respecta-la encara que la resta del projecte faci servir camelCase; qui llegeixi un docker-compose.yml o un tauler de Heroku espera aquest format.

D'on surten les variables

Això és el que sol quedar borrós. Les variables no vénen d'un sol lloc: vénen del procés pare, sigui quin sigui.

Origen Quan s'usa Exemple
Intèrpret d'ordres interactiu Proves puntuals PORT=4000 npm start
Fitxer .env + dotenv Desenvolupament local .env a l'arrel del projecte
Unitat de systemd VPS o servidor propi Environment= o EnvironmentFile=
Ecosistema de PM2 / motor de contenidors Supervisor o Docker env_production (11-03), ENV/-e (11-04)
Tauler del proveïdor PaaS Heroku, Render, Fly Variables de configuració (lliçó 11-05)
Secrets de CI Canonades secrets de GitHub Actions (lliçó 11-06)

Tots acaben al mateix lloc: process.env. Per això l'aplicació no ha de saber quin d'ells l'ha arrencada. Aquest desacoblament és justament el que fa que el mateix codi funcioni en els set escenaris.

  1. dotenv i els seus límits

En desenvolupament, exportar quinze variables a mà a cada terminal és insuportable. dotenv llegeix un fitxer .env i n'aboca el contingut a process.env. El fragment inicial de src/config/index.js, ja existent des del mòdul 6, és simplement require('dotenv').config();. Dos límits que cal tenir claríssims:

  1. dotenv no sobreescriu el que ja existeix a process.env. Si l'entorn real ja defineix PORT, el .env s'ignora per a aquesta variable. És el comportament correcte: l'entorn real mana sobre el fitxer de comoditat.
  2. dotenv no és un gestor de secrets. És un fitxer de text pla al teu disc. En producció no hi ha d'existir. El que hi ha en producció són variables injectades per l'orquestrador, el supervisor o la plataforma.

Des de Node 20 existeix a més node --env-file=.env, que fa el mateix sense dependència. Mantenim dotenv a Escena Viva perquè el projecte ja el fa servir i perquè el seu comportament és idèntic en totes les versions que suportem, però convé saber que l'alternativa nativa existeix.

.env fora, .env.example dins

El .gitignore d'Escena Viva ignora .env i totes les seves variants locals. El que sí que es versiona és .env.example: la llista completa de variables que l'aplicació necessita, amb valors falsos o buits. És documentació executable, i és el primer que mira algú que s'incorpora a l'equip.

# .env.example — copia'l a .env i omple els valors.
# MAI posis valors reals aqui: aquest fitxer SI que esta versionat.
NODE_ENV=development
PORT=3000
NIVELL_REGISTRE=debug

# Bases de dades
URL_POSTGRES=postgres://escena:escena@localhost:5432/escena_viva
URL_MONGO=mongodb://localhost:27017/escena_viva
URL_REDIS=redis://localhost:6379
# Seguretat — genera valors amb: openssl rand -hex 32
JWT_SECRET=
JWT_SECRET_ANTERIOR=
SESSIO_SECRET=
CSRF_SECRET=
# Xarxa, processos i observabilitat (11-02)
ORIGEN_CORS=http://localhost:5173
CONFIAR_EN_PROXY=false
NOMBRE_TREBALLADORS=0
CONCURRENCIA_CUA=5
TOKEN_METRIQUES=

Regla operativa: cada vegada que algú afegeix una variable al codi, l'afegeix a .env.example al mateix commit. Si no, el següent que cloni el repositori descobrirà la variable que falta quan l'aplicació peti.

  1. Validar en arrencar i fallar de pressa

Aquí hi ha el cor de la lliçó. La fallada clàssica de configuració no és que una variable falti: és quan t'assabentes que falta. Imagina que JWT_SECRET no està definit en producció. Sense validació, l'aplicació arrenca perfectament. Serveix el catàleg, mostra les set sessions del Festival de Jazz, deixa navegar. Tres hores després, la Lucía intenta iniciar sessió per comprar dues entrades i jsonwebtoken llança secretOrPrivateKey must have a value. Un error 500, una clienta perduda i un registre que no diu res útil. Amb validació en arrencar, l'aplicació no arrenca. El desplegament falla al primer segon, la plataforma no envia trànsit a la instància trencada i el missatge diu exactament què falta. Això és fallar de pressa. Ampliem src/config/index.js amb un esquema zod (la mateixa llibreria que ja fem servir per validar peticions al mòdul 6, així que no afegim dependències).

// src/config/index.js
'use strict';

require('dotenv').config();

const { z } = require('zod');

// Ajudes de conversio: recorda que TOT arriba com a cadena.
const comEnter = (perDefecte) =>
  z.coerce.number().int().nonnegative().default(perDefecte);

const comBoolea = (perDefecte) =>
  z.enum(['true', 'false']).default(String(perDefecte)).transform((v) => v === 'true');

// Un secret util te longitud suficient: 32 caracters hex com a minim.
const secretFort = z.string().min(32, 'minim 32 caracters: openssl rand -hex 32');

const esquemaConfiguracio = z.object({
  NODE_ENV: z.enum(['development', 'test', 'staging', 'production']).default('development'),
  PORT: comEnter(3000),
  NIVELL_REGISTRE: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),

  URL_POSTGRES: z.string().url(),
  URL_MONGO: z.string().url(),
  URL_REDIS: z.string().url(),

  JWT_SECRET: secretFort,
  // Opcional: nomes existeix durant una rotacio (apartat 8).
  JWT_SECRET_ANTERIOR: secretFort.optional(),
  SESSIO_SECRET: secretFort,
  CSRF_SECRET: secretFort,

  ORIGEN_CORS: z.string().default('http://localhost:5173'),
  CONFIAR_EN_PROXY: comBoolea(false),

  NOMBRE_TREBALLADORS: comEnter(0),
  CONCURRENCIA_CUA: comEnter(5),

  TOKEN_METRIQUES: z.string().min(16).optional(),
});

const resultat = esquemaConfiguracio.safeParse(process.env);

if (!resultat.success) {
  // Ni logger ni res elaborat: aqui encara no hi ha aplicacio.
  const problemes = resultat.error.issues
    .map((i) => `  - ${i.path.join('.')}: ${i.message}`)
    .join('\n');
  process.stderr.write(
    `\nConfiguracio invalida. Escena Viva no pot arrencar:\n${problemes}\n\n` +
      "Revisa .env.example i les variables de l'entorn de desplegament.\n\n"
  );
  process.exit(1);
}

const crua = resultat.data;

// La forma de l'objecte NO es la de l'entorn: la resta del codi demana
// configuracio.seguretat.jwtSecret, mai process.env.JWT_SECRET.
const configuracio = Object.freeze({
  entorn: crua.NODE_ENV,
  esProduccio: crua.NODE_ENV === 'production',
  esProva: crua.NODE_ENV === 'test',
  port: crua.PORT,
  nivellRegistre: crua.NIVELL_REGISTRE,
  confiarEnProxy: crua.CONFIAR_EN_PROXY,
  origensCors: crua.ORIGEN_CORS.split(',').map((origen) => origen.trim()),
  baseDades: Object.freeze({
    postgres: crua.URL_POSTGRES, mongo: crua.URL_MONGO, redis: crua.URL_REDIS,
  }),
  seguretat: Object.freeze({
    jwtSecret: crua.JWT_SECRET,
    jwtSecretAnterior: crua.JWT_SECRET_ANTERIOR,
    sessioSecret: crua.SESSIO_SECRET,
    csrfSecret: crua.CSRF_SECRET,
  }),
  processos: Object.freeze({
    nombreTreballadors: crua.NOMBRE_TREBALLADORS,
    concurrenciaCua: crua.CONCURRENCIA_CUA,
  }),
  observabilitat: Object.freeze({ tokenMetriques: crua.TOKEN_METRIQUES }),
});

module.exports = { configuracio };

Què fa cada decisió:

  • z.coerce.number() converteix la cadena a nombre dins de l'esquema, així que no tornes a escriure mai Number(process.env.ALGUNA_COSA) dispers pel codi.
  • comBoolea accepta només 'true' o 'false': si algú escriu CONFIAR_EN_PROXY=si, l'arrencada falla amb un missatge clar en comptes d'interpretar-ho com a cert.
  • .default() centralitza els valors per defecte en un únic lloc. El || 4 dispers desapareix, i amb ell el problema del zero.
  • safeParse + process.exit(1) converteix qualsevol problema en una mort immediata amb codi de sortida diferent de zero, que és el que PM2, Docker i les PaaS interpreten com a «arrencada fallida».
  • Object.freeze a tots els nivells impedeix que un mòdul modifiqui la configuració en calent i deixi la resta del procés veient una altra cosa.
  • La forma de l'objecte exportat no és la de l'entorn. La resta del codi demana configuracio.seguretat.jwtSecret, no process.env.JWT_SECRET. Si demà reanomenem la variable, es toca un fitxer.

Amb JWT_SECRET absent, l'arrencada produeix Configuracio invalida. Escena Viva no pot arrencar: - JWT_SECRET: Required. Quaranta caràcters de sortida que estalvien una tarda sencera.

Regla d'or: process.env es llegeix exclusivament a src/config/index.js. En qualsevol altre fitxer està prohibit. Ho pots vigilar amb una regla d'ESLint (no-restricted-properties) o amb un grep a la canonada de CI de la lliçó 11-06.

  1. Configuració per entorn sense duplicar fitxers

La temptació és crear config.development.js, config.staging.js i config.production.js. És un error, perquè duplica estructura: quan afegeixes una variable has de tocar tres fitxers i sempre n'oblides un, i perquè l'única diferència real entre entorns són els valors, no la forma. A Escena Viva hi ha un sol esquema i els valors arriben de fora:

Entorn NODE_ENV D'on surten els valors
Desenvolupament development .env local amb dotenv
Prova test .env.test (mòdul 9) i serveis de CI
Preproducció staging Tauler del proveïdor o secrets de l'orquestrador
Producció production Gestor de secrets + variables de la plataforma

Per què NODE_ENV=production importa més del que sembla

NODE_ENV no és una variable com les altres: mig ecosistema la mira.

  • Express desactiva la traça d'error a les respostes, activa la memòria cau de vistes i redueix feina per petició. Executar Express amb NODE_ENV sense definir en producció és més lent i més filtrador.
  • Moltes llibreries (validadors, motors de plantilles, React al frontal) desactiven comprovacions de desenvolupament i avisos.
  • npm: npm ci --omit=dev instal·la només les dependències de producció. És el que farem a la imatge Docker de 11-04 i a la construcció de la PaaS de 11-05. I per això tot el que l'aplicació necessita en temps d'execució ha d'estar a dependencies, mai a devDependencies. Un require d'un paquet de desenvolupament peta en producció i enlloc més.
  • El nostre propi codi: configuracio.esProduccio decideix si pino-pretty està actiu (11-02), si les galetes porten secure, si s'exposen les traces.

Només valen els quatre valors de l'enum. Un NODE_ENV=prod mal escrit ja no arrenca l'aplicació en comptes de deixar-la en mode desenvolupament silenciosament.

  1. Què no va mai en una variable d'entorn

Sí No
Cadenes de connexió, claus, tokens Fitxers grans o binaris (certificats PEM sencers)
Interruptors de comportament per entorn Lògica de negoci disfressada de configuració
Noms de màquina, ports, orígens Dades personals d'usuaris
Nivell de registre, indicadors de funcionalitat simples Estructures complexes en JSON codificat

Sobre l'últim punt: CONFIG_SALES={"teatro-almendra":{"aforament":800}} és un senyal d'alarma. Si la configuració necessita estructura, el que necessites és un fitxer muntat (que pot venir d'un secret de l'orquestrador) o una taula a la base de dades, no una variable d'entorn amb JSON a dins que ningú pot llegir ni depurar.

  1. Gestió de secrets de debò

Les variables d'entorn estan millor que el codi versionat, però no són segures. Es filtren per llocs que no esperes:

  • Bolcats de procés. Un core dump conté el bloc d'entorn complet.
  • Registres i traces. Un console.log(process.env) en una depuració d'urgència, o una llibreria que aboca el context en un informe d'error, i el teu JWT_SECRET acaba al servei de registre de tercers.
  • /proc/<pid>/environ. A Linux, qualsevol procés del mateix usuari pot llegir l'entorn d'un altre.
  • Imatges de contenidor. Un ENV JWT_SECRET=... al Dockerfile queda gravat en una capa de la imatge, i la imatge es publica en un registre (11-04).
  • La sortida d'un error. Alguns clients de base de dades inclouen l'URL completa —amb usuari i contrasenya— al missatge d'excepció.
  • Subprocessos. Tot fill hereta l'entorn del pare per defecte.

Per això existeixen els gestors de secrets. El panorama honest:

Solució Com funciona A favor En contra
Variables del proveïdor (Heroku, Render, Fly) Tauler o CLI, injectades en arrencar Zero infraestructura, immediat Sense rotació automàtica ni auditoria fina
HashiCorp Vault Servei dedicat; l'app demana el secret amb un token Rotació, auditoria, secrets dinàmics i de vida curta Operar Vault és un projecte en si mateix
AWS Secrets Manager / GCP Secret Manager Servei gestionat del proveïdor, amb IAM Integració nativa, rotació programada Lliga al proveïdor, cost per secret i accés
Fitxers muntats (secrets de Kubernetes/Swarm) El secret apareix com a fitxer al contenidor No apareix a environ ni a la imatge Requereix orquestrador; cal llegir el fitxer
Xifratge al repositori (SOPS, git-crypt) Secrets xifrats versionats; es desxifren en desplegar Historial i revisió per PR dels canvis La clau mestra continua havent de viure en algun lloc

Escena Viva comença amb variables del proveïdor (11-05) perquè la mida del projecte no justifica operar Vault, i deixa el camí obert: com que tota la lectura passa per src/config/index.js, migrar a fitxers muntats és canviar quinze línies en un únic fitxer. Un patró intermedi molt útil és el sufix _FILE: si existeix JWT_SECRET_FILE, es llegeix el contingut d'aquell fitxer; si no, es fa servir JWT_SECRET. Així la mateixa imatge serveix per a una PaaS amb variables i per a un orquestrador amb secrets muntats.

  1. Rotació de secrets sense parada

Els secrets caduquen. Es roten perquè algú deixa l'equip, perquè ho exigeix una auditoria o perquè s'han filtrat. El problema és que canviar JWT_SECRET de cop invalida tots els tokens en circulació: la Lucía, en Marc i els organitzadors de les tres sales queden desconnectats a mitja compra. La solució és acceptar dues claus durant la transició. Se signa sempre amb la nova i es verifica contra totes dues.

// src/serveis/tokens.js (fragment adaptat per a la rotacio)
'use strict';

const jwt = require('jsonwebtoken');
const { configuracio } = require('../config/index.js');
const { ErrorDAutenticacio } = require('../errors.js');
const { jwtSecret, jwtSecretAnterior } = configuracio.seguretat;

// Se signa SEMPRE amb el secret vigent.
const signarAcces = (carrega) => jwt.sign(carrega, jwtSecret, { expiresIn: '15m' });

function verificarAcces(token) {
  // Ordre important: primer el vigent (cas majoritari).
  for (const clau of [jwtSecret, jwtSecretAnterior].filter(Boolean)) {
    try {
      return jwt.verify(token, clau);
    } catch (error) {
      // Token caducat o malformat: no el tapem.
      if (error.name !== 'JsonWebTokenError') throw error;
    }
  }
  throw new ErrorDAutenticacio('Token no valid');
}

module.exports = { signarAcces, verificarAcces };

El procediment complet, amb tokens d'accés de 15 minuts: (1) generar la clau nova amb openssl rand -hex 32; (2) posar el valor actual a JWT_SECRET_ANTERIOR i el nou a JWT_SECRET; (3) reiniciar amb recàrrega sense talls (lliçó 11-03), i des d'aquell instant se signa amb la nova i s'accepta la vella; (4) esperar més que la vida del token més llarg —amb accés de 15 minuts i refresc de 7 dies, fins que caduquin els refrescos o se'n forci la rotació—; i (5) esborrar JWT_SECRET_ANTERIOR i reiniciar un altre cop. Fixa't que l'esquema ja ho suporta: JWT_SECRET_ANTERIOR és .optional(), així que l'aplicació arrenca igual amb una clau o amb dues. La rotació no requereix tocar codi.

  1. Quan un secret es filtra

Passa. Algú enganxa l'URL de PostgreSQL en un xat, o fa commit del .env un divendres. L'ordre de les accions importa:

  1. Revocar i rotar primer. El secret filtrat és vàlid fins que deixa de ser-ho; tota la resta pot esperar.
  2. Revisar accessos després, amb els registres d'auditoria del mòdul 8: hi va haver connexions des d'IP desconegudes?, es van emetre tokens estranys?
  3. Netejar l'historial al final, sabent que no n'hi ha prou. Reescriure l'historial de Git (git filter-repo) no esborra el secret dels clons que ja existeixen, ni dels forks, ni de la memòria cau de la interfície web, ni dels rastrejadors automàtics que escanegen GitHub buscant credencials. Un secret que ha estat en un repositori remot està compromès per sempre. Esborrar el commit és higiene, no remei.
  4. Evitar la reincidència. Un ganxo de pre-commit que detecti patrons de credencials (el projecte ja té husky des del mòdul 9) i escaneig de secrets a la canonada de CI (11-06).

  1. Configuració en calent enfront de reinici

Hi ha dues maneres d'aplicar un canvi de configuració: recarregar-la al procés viu, o reiniciar el procés.

En calent Reinici
Complexitat Alta: cada mòdul ha de reaccionar al canvi Nul·la
Estat Risc d'inconsistència entre mòduls Tot coherent des del segon zero
Tall de servei Cap Cap si hi ha recàrrega sense talls
Auditoria Difícil saber quins valors hi havia a cada moment L'arrencada en deixa constància

Escena Viva reinicia, i per això configuracio està congelat. La raó és que ja tenim aturada ordenada (mòdul 6) i tindrem recàrrega seqüencial de treballadors (11-03): reiniciar costa segons i no perd ni una petició. La complexitat de recarregar en calent només es justifica quan l'arrencada és caríssima, que no és el nostre cas. Nota important: un indicador de funcionalitat no és configuració de desplegament. Si vols activar la venda anticipada del Festival de Jazz a les 10:00 sense reiniciar, això va a base de dades o a un servei d'indicadors, no a process.env.

  1. Taula de referència de les variables d'Escena Viva

Aquesta taula és la referència que farem servir a la resta del mòdul: el .env.example, l'ecosystem.config.js de PM2, el docker-compose.yml, el tauler de la PaaS i els secrets de CI en deriven tots.

Variable Tipus Obligatòria Per defecte On es fa servir
NODE_ENV enum No development Express, npm, configuracio.esProduccio
PORT enter No 3000 src/servidor.js
NIVELL_REGISTRE enum No info pino (11-02)
URL_POSTGRES url Sí — src/db/sequelize.js
URL_MONGO url Sí — src/db/connexio.js
URL_REDIS url Sí — src/db/redis.js, sessió, límits, cues
JWT_SECRET secret ≥32 Sí — src/serveis/tokens.js
JWT_SECRET_ANTERIOR secret ≥32 No — Rotació (apartat 8)
SESSIO_SECRET secret ≥32 Sí — src/middleware/sessio.js
CSRF_SECRET secret ≥32 Sí — src/middleware/csrf.js
ORIGEN_CORS llista No http://localhost:5173 src/middleware/cors.js
CONFIAR_EN_PROXY boolea No false trust proxy (11-05)
NOMBRE_TREBALLADORS enter No 0 (= nuclis) src/cluster.js, pool de Sequelize
CONCURRENCIA_CUA enter No 5 src/processos/consumidor-entrades.js
TOKEN_METRIQUES secret ≥16 No — /metriques protegit (11-02)

Errors Comuns i Consells

  • Llegir process.env fora de src/config/index.js. És l'error que més mal fa a llarg termini: es perd la validació, els valors per defecte es dispersen i ningú sap quines variables fa servir l'aplicació. Prohibeix-ho amb lint.
  • Confondre «no és al codi» amb «és segur». Una variable d'entorn és visible a /proc, en bolcats i en qualsevol abocament accidental. Tracta-la com un secret en trànsit, no com una caixa forta.
  • Consell: genera tots els secrets amb openssl rand -hex 32 i no reutilitzis mai el mateix valor per a JWT_SECRET, SESSIO_SECRET i CSRF_SECRET: si un es filtra, es filtren els tres. I actualitza .env.example i la taula de l'apartat 11 al mateix commit en què afegeixis una variable.

Exercicis

Exercici 1 — Diagnòstic de configuració

Escriu un script scripts/comprovar-configuracio.js que carregui src/config/index.js i imprimeixi una taula amb totes les variables de l'esquema indicant, per a cadascuna, si ve de l'entorn o d'un valor per defecte, censurant els secrets. Ha de sortir amb codi 1 si la configuració és invàlida (aprofitant el process.exit(1) que ja fa el mòdul).

Exercici 2 — Suport de _FILE

Amplia src/config/index.js perquè qualsevol variable pugui venir d'un fitxer: si existeix <NOM>_FILE, es llegeix el contingut d'aquella ruta (retallant el salt de línia final) i es fa servir com a valor de <NOM>. Aplica-ho abans de validar amb zod.

Exercici 3 — Rotació simulada

Escriu una prova d'integració que verifiqui que un token signat amb JWT_SECRET_ANTERIOR continua sent acceptat per verificarAcces, i que un de signat amb una tercera clau desconeguda és rebutjat amb ErrorDAutenticacio.

Solucions

Exercici 1. La clau és no tornar a llegir process.env a l'script llevat que sigui per saber-ne l'origen, i censurar per nom:

// scripts/comprovar-configuracio.js
'use strict';

// Si la configuracio fos invalida, aquest require ja hauria sortit amb codi 1.
const { configuracio } = require('../src/config/index.js');

const SENSIBLES = /SECRET|TOKEN|PASSWORD|URL_POSTGRES|URL_MONGO/;
const NOMS = [
  'NODE_ENV', 'PORT', 'NIVELL_REGISTRE', 'URL_POSTGRES', 'URL_MONGO',
  'URL_REDIS', 'JWT_SECRET', 'JWT_SECRET_ANTERIOR', 'SESSIO_SECRET',
  'CSRF_SECRET', 'ORIGEN_CORS', 'CONFIAR_EN_PROXY', 'NOMBRE_TREBALLADORS',
  'CONCURRENCIA_CUA', 'TOKEN_METRIQUES',
];

const censurar = (nom, valor) =>
  valor === undefined ? '(sense definir)' : SENSIBLES.test(nom) ? '********' : String(valor);

console.table(NOMS.map((nom) => ({
  variable: nom,
  origen: process.env[nom] === undefined ? 'per defecte' : 'entorn',
  valor: censurar(nom, process.env[nom]),
})));

Exercici 2. Es preprocessa process.env abans del safeParse:

const fs = require('node:fs');

function resoldreFitxers(entorn) {
  const resolt = { ...entorn };
  for (const [clau, valor] of Object.entries(entorn)) {
    if (!clau.endsWith('_FILE') || !valor) continue;
    try {
      // Lectura sincrona a proposit: som a l'arrencada.
      resolt[clau.slice(0, -'_FILE'.length)] = fs.readFileSync(valor, 'utf8').trimEnd();
    } catch (error) {
      process.stderr.write(`No s'ha pogut llegir ${clau}=${valor}: ${error.message}\n`);
      process.exit(1);
    }
  }
  return resolt;
}

const resultat = esquemaConfiguracio.safeParse(resoldreFitxers(process.env));

Es llegeix de forma síncrona expressament: som a l'arrencada, abans que existeixi bucle d'esdeveniments amb feina, i volem fallar abans de continuar.

Exercici 3. Amb mocha i chai, signant a mà amb cada clau:

const { expect } = require('chai');
const jwt = require('jsonwebtoken');
const { configuracio } = require('../../src/config/index.js');
const { verificarAcces } = require('../../src/serveis/tokens.js');

describe('rotacio de JWT_SECRET', () => {
  const carrega = { sub: 'usr-lucia', rol: 'assistent' };

  it('accepta un token signat amb el secret anterior', () => {
    const anterior = configuracio.seguretat.jwtSecretAnterior;
    expect(anterior, 'defineix JWT_SECRET_ANTERIOR a .env.test').to.be.a('string');
    const token = jwt.sign(carrega, anterior, { expiresIn: '15m' });
    expect(verificarAcces(token)).to.include({ sub: 'usr-lucia' });
  });
  it('rebutja un token signat amb una clau desconeguda', () => {
    const token = jwt.sign(carrega, 'x'.repeat(32), { expiresIn: '15m' });
    expect(() => verificarAcces(token)).to.throw(/no valid/i);
  });
});

Conclusió

La configuració d'Escena Viva ha deixat de ser un .env al teu portàtil per convertir-se en un contracte explícit: un esquema zod que enumera cada variable, en converteix el tipus, aplica valors per defecte i mata el procés amb un missatge comprensible si falta un secret. Un únic punt de lectura de process.env, un .env.example que documenta el contracte, una taula de referència que servirà de base per al fitxer de PM2, el Dockerfile, el docker-compose.yml, el tauler de la PaaS i els secrets de CI. I, a més, una estratègia de rotació que permet canviar la clau de signatura dels JWT sense desconnectar ningú. Amb la configuració resolta, apareix la següent pregunta incòmoda: quan alguna cosa falli en producció a les tres de la matinada —i fallarà—, com te n'assabentes? A la propera lliçó, Registre i Monitoratge en Producció, substituïm els console.log per registre estructurat en JSON amb pino, pengem el registrador fill de l'idPeticio que arrosseguem des del mòdul 6 per poder seguir una compra fallida per tots els seus rastres, exposem mètriques amb prom-client —inclòs el retard del bucle d'esdeveniments que ja sabem mesurar— i implementem les sondes /salut/viu i /salut/preparat que la resta del mòdul donarà per suposades.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats