From bbd78d73d26aa0242e0f084b97948ca329af33ec Mon Sep 17 00:00:00 2001 From: amberly-d Date: Mon, 22 Jun 2026 11:39:47 +0100 Subject: [PATCH 1/2] feat(config): add Joi environment variable validation schema on startup Closes #450 --- meridian-api/package.json | 1 + meridian-api/src/app.module.ts | 6 +++ meridian-api/src/config/env.validation.ts | 60 +++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 meridian-api/src/config/env.validation.ts diff --git a/meridian-api/package.json b/meridian-api/package.json index feaa5f8d..20772090 100644 --- a/meridian-api/package.json +++ b/meridian-api/package.json @@ -35,6 +35,7 @@ "@willsoto/nestjs-prometheus": "^6.1.0", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", + "joi": "^17.13.3", "class-validator": "^0.14.1", "helmet": "^8.2.0", "mysql2": "^3.11.5", diff --git a/meridian-api/src/app.module.ts b/meridian-api/src/app.module.ts index 858e5740..3cf954b0 100644 --- a/meridian-api/src/app.module.ts +++ b/meridian-api/src/app.module.ts @@ -8,6 +8,7 @@ import { DataSource } from 'typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { envValidationSchema } from './config/env.validation'; import { UsersModule } from './users/users.module'; import { PostModule } from './post/post.module'; @@ -36,6 +37,11 @@ import { PrometheusModule } from '@willsoto/nestjs-prometheus'; ConfigModule.forRoot({ isGlobal: true, envFilePath: `.env.${process.env.NODE_ENV || 'development'}`, + validationSchema: envValidationSchema, + validationOptions: { + allowUnknown: true, + abortEarly: false, + }, }), /** diff --git a/meridian-api/src/config/env.validation.ts b/meridian-api/src/config/env.validation.ts new file mode 100644 index 00000000..f3259e75 --- /dev/null +++ b/meridian-api/src/config/env.validation.ts @@ -0,0 +1,60 @@ +import * as Joi from 'joi'; + +export const envValidationSchema = Joi.object({ + NODE_ENV: Joi.string() + .valid('development', 'production', 'test') + .default('development'), + + PORT: Joi.number().integer().min(1).max(65535).default(3000), + + ALLOWED_ORIGINS: Joi.string().optional().allow(''), + + // Railway-style single connection URL (optional; takes precedence over individual vars) + DATABASE_URL: Joi.string().uri().optional(), + + // Individual Postgres vars (required when DATABASE_URL is absent) + POSTGRES_HOST: Joi.string().when('DATABASE_URL', { + is: Joi.exist(), + then: Joi.optional(), + otherwise: Joi.string().required(), + }), + POSTGRES_PORT: Joi.number().integer().min(1).max(65535).default(5432), + POSTGRES_USER: Joi.string().when('DATABASE_URL', { + is: Joi.exist(), + then: Joi.optional(), + otherwise: Joi.string().required(), + }), + POSTGRES_PASSWORD: Joi.string().when('DATABASE_URL', { + is: Joi.exist(), + then: Joi.optional(), + otherwise: Joi.string().required(), + }), + POSTGRES_DB: Joi.string().when('DATABASE_URL', { + is: Joi.exist(), + then: Joi.optional(), + otherwise: Joi.string().required(), + }), + POSTGRES_SYNC: Joi.string().valid('true', 'false').default('false'), + POSTGRES_LOAD: Joi.string().valid('true', 'false').default('true'), + + // JWT + JWT_SECRET: Joi.string().min(16).required(), + JWT_TOKEN_AUDIENCE: Joi.string().required(), + JWT_TOKEN_ISSUER: Joi.string().required(), + JWT_ACCESS_TOKEN_TTL: Joi.number().integer().positive().default(360), + JWT_REFRESH_TOKEN_TTL: Joi.number().integer().positive().default(7776000), + + // Email verification + VERIFICATION_TOKEN_TTL_HOURS: Joi.number().integer().positive().default(24), + + // Application + APP_URL: Joi.string().uri().default('http://localhost:3000'), + + // File upload + STORAGE_PROVIDER: Joi.string().valid('local', 's3').default('local'), + UPLOAD_MAX_SIZE_MB: Joi.number().positive().default(5), + UPLOAD_S3_BUCKET: Joi.string().optional().allow(''), + UPLOAD_S3_REGION: Joi.string().optional().allow(''), + UPLOAD_S3_ACCESS_KEY_ID: Joi.string().optional().allow(''), + UPLOAD_S3_SECRET_ACCESS_KEY: Joi.string().optional().allow(''), +}); From 8ffa913bd1896937926123f9c2fae55e02b0c36a Mon Sep 17 00:00:00 2001 From: amberly-d Date: Mon, 22 Jun 2026 21:13:33 +0000 Subject: [PATCH 2/2] chore: update package-lock.json with joi dependency --- meridian-api/package-lock.json | 51 +++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/meridian-api/package-lock.json b/meridian-api/package-lock.json index 24446c27..d61ff351 100644 --- a/meridian-api/package-lock.json +++ b/meridian-api/package-lock.json @@ -26,6 +26,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "helmet": "^8.2.0", + "joi": "^17.13.3", "mysql2": "^3.11.5", "pg": "^8.13.1", "prom-client": "^15.1.3", @@ -1857,6 +1858,21 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -3373,6 +3389,27 @@ "selderee": "~0.12.0" } }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, "node_modules/@sinclair/typebox": { "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", @@ -8090,7 +8127,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -9911,6 +9947,19 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "17.13.4", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.4.tgz", + "integrity": "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-beautify": { "version": "1.15.4", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",