fix(api,web): env startup validation, QueryClient defaults, warn on missing REDIS_URL
- Throw at startup in production if REDIS_URL/DATABASE_URL/NEXTAUTH_SECRET missing - Warn in development when REDIS_URL falls back to localhost - QueryClient: add gcTime, disable refetchOnWindowFocus, skip retry on 4xx Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,11 @@
|
|||||||
export async function register() {
|
export async function register() {
|
||||||
|
// Validate required env vars at startup — throws immediately in production
|
||||||
|
// if REDIS_URL, DATABASE_URL, or NEXTAUTH_SECRET are missing.
|
||||||
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||||
|
const { assertProductionEnv } = await import("@capakraken/api");
|
||||||
|
assertProductionEnv();
|
||||||
|
}
|
||||||
|
|
||||||
// Only load Sentry in production — the worker.js crash in dev mode
|
// Only load Sentry in production — the worker.js crash in dev mode
|
||||||
// (vendor-chunks/lib/worker.js MODULE_NOT_FOUND) makes the dev server unstable
|
// (vendor-chunks/lib/worker.js MODULE_NOT_FOUND) makes the dev server unstable
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
|||||||
@@ -50,12 +50,18 @@ export function TRPCProvider({ children }: { children: React.ReactNode }) {
|
|||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
staleTime: 60_000, // 60 seconds — reduces refetches on navigation
|
staleTime: 60_000, // 60 seconds — reduces refetches on navigation
|
||||||
|
gcTime: 5 * 60_000, // 5 minutes — keep inactive queries in memory
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
retry: (failureCount, error) => {
|
retry: (failureCount, error) => {
|
||||||
// Never retry UNAUTHORIZED — redirect immediately instead
|
// Never retry UNAUTHORIZED — redirect immediately instead
|
||||||
if (isUnauthorizedTrpcError(error)) return false;
|
if (isUnauthorizedTrpcError(error)) return false;
|
||||||
return failureCount < 1;
|
// Don't retry on 4xx errors (auth, not found, bad input)
|
||||||
|
if (error instanceof TRPCClientError) {
|
||||||
|
const code = error.data?.httpStatus as number | undefined;
|
||||||
|
if (code !== undefined && code >= 400 && code < 500) return false;
|
||||||
|
}
|
||||||
|
return failureCount < 2;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ export { lookupRate, type RateCardLookupParams, type RateCardLookupResult } from
|
|||||||
export { autoImportPublicHolidays, type AutoImportResult } from "./lib/holiday-auto-import.js";
|
export { autoImportPublicHolidays, type AutoImportResult } from "./lib/holiday-auto-import.js";
|
||||||
export { createAuditEntry, computeDiff, generateSummary } from "./lib/audit.js";
|
export { createAuditEntry, computeDiff, generateSummary } from "./lib/audit.js";
|
||||||
export { loggedAiCall } from "./ai-client.js";
|
export { loggedAiCall } from "./ai-client.js";
|
||||||
|
export { assertProductionEnv } from "./lib/env.js";
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
const REDIS_URL = process.env["REDIS_URL"] ?? "redis://localhost:6380";
|
const REDIS_URL = process.env["REDIS_URL"] ?? (
|
||||||
|
process.env["NODE_ENV"] !== "production"
|
||||||
|
? (console.warn("[env] REDIS_URL not set, using localhost fallback"), "redis://localhost:6380")
|
||||||
|
: (() => { throw new Error("REDIS_URL required in production"); })()
|
||||||
|
);
|
||||||
const KEY_PREFIX = "dashboard:";
|
const KEY_PREFIX = "dashboard:";
|
||||||
const DEFAULT_TTL_SECONDS = 60;
|
const DEFAULT_TTL_SECONDS = 60;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Startup environment validation.
|
||||||
|
* Call assertProductionEnv() early in the server lifecycle to surface missing
|
||||||
|
* critical configuration before any requests are served.
|
||||||
|
*/
|
||||||
|
export function assertProductionEnv(): void {
|
||||||
|
if (process.env["NODE_ENV"] !== "production") return;
|
||||||
|
|
||||||
|
const required = ["REDIS_URL", "DATABASE_URL", "NEXTAUTH_SECRET"] as const;
|
||||||
|
const missing = required.filter((k) => !process.env[k]);
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new Error(`Missing required env vars: ${missing.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -180,7 +180,11 @@ export function cancelPendingEvents(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Redis connection — use env var REDIS_URL or fallback to default dev URL
|
// Redis connection — use env var REDIS_URL or fallback to default dev URL
|
||||||
const REDIS_URL = process.env["REDIS_URL"] ?? "redis://localhost:6380";
|
const REDIS_URL = process.env["REDIS_URL"] ?? (
|
||||||
|
process.env["NODE_ENV"] !== "production"
|
||||||
|
? (console.warn("[env] REDIS_URL not set, using localhost fallback"), "redis://localhost:6380")
|
||||||
|
: (() => { throw new Error("REDIS_URL required in production"); })()
|
||||||
|
);
|
||||||
const CHANNEL = "capakraken:sse";
|
const CHANNEL = "capakraken:sse";
|
||||||
|
|
||||||
let publisher: Redis | null = null;
|
let publisher: Redis | null = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user