feat: whyrating - initial project from turbostarter boilerplate
This commit is contained in:
137
packages/shared/src/validate-env/index.ts
Normal file
137
packages/shared/src/validate-env/index.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Runtime Environment Validation
|
||||
*
|
||||
* Runs once on server startup (via Next.js instrumentation).
|
||||
* Validates that required env vars are present BEFORE the app serves requests.
|
||||
*
|
||||
* Design:
|
||||
* - Build time: all env vars are optional (so `next build` works without them)
|
||||
* - Runtime: this module checks process.env directly and fails fast with clear messages
|
||||
* - Feature-gated: optional features only require their vars when explicitly enabled
|
||||
*/
|
||||
|
||||
type EnvRule = {
|
||||
key: string;
|
||||
required: boolean;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
type FeatureGate = {
|
||||
name: string;
|
||||
/** The env var that enables this feature (if set and non-empty, feature is "on") */
|
||||
gate: string;
|
||||
vars: string[];
|
||||
};
|
||||
|
||||
/** Always required in production */
|
||||
const REQUIRED_VARS: EnvRule[] = [
|
||||
{
|
||||
key: "DATABASE_URL",
|
||||
required: true,
|
||||
reason: "App cannot start without a database connection",
|
||||
},
|
||||
{
|
||||
key: "BETTER_AUTH_SECRET",
|
||||
required: true,
|
||||
reason: "Auth sessions will be insecure without a proper secret",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Feature-gated vars: only required when the gate var is set.
|
||||
* This lets you skip entire features (S3, Stripe, email) without errors.
|
||||
*/
|
||||
const FEATURE_GATES: FeatureGate[] = [
|
||||
{
|
||||
name: "S3 Storage",
|
||||
gate: "S3_BUCKET",
|
||||
vars: ["S3_ENDPOINT", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"],
|
||||
},
|
||||
{
|
||||
name: "Stripe Billing",
|
||||
gate: "STRIPE_SECRET_KEY",
|
||||
vars: ["STRIPE_WEBHOOK_SECRET"],
|
||||
},
|
||||
{
|
||||
name: "Resend Email",
|
||||
gate: "RESEND_API_KEY",
|
||||
vars: ["EMAIL_FROM"],
|
||||
},
|
||||
{
|
||||
name: "Nodemailer Email",
|
||||
gate: "NODEMAILER_HOST",
|
||||
vars: ["NODEMAILER_PORT", "NODEMAILER_USER", "NODEMAILER_PASSWORD", "EMAIL_FROM"],
|
||||
},
|
||||
];
|
||||
|
||||
/** Vars that trigger a warning (not a crash) if missing */
|
||||
const WARN_VARS = [
|
||||
{ key: "BETTER_AUTH_TRUSTED_ORIGINS", reason: "CSRF protection may reject external requests" },
|
||||
{ key: "EMAIL_FROM", reason: "Emails will use 'noreply@example.com' as sender" },
|
||||
];
|
||||
|
||||
function getEnv(key: string): string | undefined {
|
||||
const val = process.env[key];
|
||||
if (!val || val.trim() === "") return undefined;
|
||||
return val;
|
||||
}
|
||||
|
||||
export function validateRuntimeEnv(): void {
|
||||
// Skip in development or when explicitly disabled
|
||||
if (process.env.NODE_ENV !== "production") return;
|
||||
if (process.env.SKIP_ENV_VALIDATION === "1" || process.env.SKIP_ENV_VALIDATION === "true") return;
|
||||
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Check required vars
|
||||
for (const rule of REQUIRED_VARS) {
|
||||
if (!getEnv(rule.key)) {
|
||||
errors.push(` ${rule.key} — ${rule.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check feature-gated vars
|
||||
for (const feature of FEATURE_GATES) {
|
||||
if (getEnv(feature.gate)) {
|
||||
for (const varName of feature.vars) {
|
||||
if (!getEnv(varName)) {
|
||||
errors.push(` ${varName} — Required when ${feature.name} is enabled (${feature.gate} is set)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check warning vars
|
||||
for (const rule of WARN_VARS) {
|
||||
if (!getEnv(rule.key)) {
|
||||
warnings.push(` ${rule.key} — ${rule.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Print warnings
|
||||
if (warnings.length > 0) {
|
||||
console.warn(
|
||||
`\n⚠️ Environment warnings:\n${warnings.join("\n")}\n`
|
||||
);
|
||||
}
|
||||
|
||||
// Fail on errors
|
||||
if (errors.length > 0) {
|
||||
const msg = [
|
||||
"",
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
||||
"❌ Missing required environment variables:",
|
||||
"",
|
||||
...errors,
|
||||
"",
|
||||
"The app cannot start safely without these.",
|
||||
"Set them in your docker-compose or .env file.",
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
// Throw instead of process.exit — works in both Node and Edge runtimes
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user