Files
tiennm99 10a3693f1b fix(scaffold): apply phase-0 review findings
- env: cross-check VERCEL_ENV against NEXT_PUBLIC_APP_ENV at boot so prod
  credentials cannot silently write into a dev keyspace
- upstash: tighten cache-key regex (kebab + colon only); split SCAN
  patterns into their own validator so glob '*' is allowed only there
- eslint: forbid raw @upstash/redis, @upstash/ratelimit, @supabase/supabase-js
  imports outside the named factory files
- supabase/admin: harmonize 'use cache' guidance with CONTRIBUTING.md
  (safe inside cache; partition key on identity for user-specific reads)
- app/layout: clarify global-error.tsx vs error.tsx shell requirements
  given the passthrough root layout
- readme: Next.js 15 -> 16 (matches scaffolded version)
2026-05-25 15:56:54 +07:00

58 lines
2.0 KiB
TypeScript

import "server-only";
import { z } from "zod";
import { APP_SLUG, SUPABASE_SCHEMA, clientEnv } from "./client";
const VERCEL_TO_APP_ENV = {
production: "prod",
preview: "preview",
development: "dev",
} as const;
const serverSchema = z
.object({
NODE_ENV: z.enum(["development", "preview", "production", "test"]).default("development"),
// Vercel sets VERCEL_ENV automatically on every deploy. Absent → local dev.
VERCEL_ENV: z.enum(["production", "preview", "development"]).optional(),
SUPABASE_SECRET_KEY: z.string().min(1),
UPSTASH_REDIS_REST_URL: z.url(),
UPSTASH_REDIS_REST_TOKEN: z.string().min(1),
QSTASH_URL: z.url().optional(),
QSTASH_TOKEN: z.string().optional(),
QSTASH_CURRENT_SIGNING_KEY: z.string().optional(),
QSTASH_NEXT_SIGNING_KEY: z.string().optional(),
})
.superRefine((env, ctx) => {
// Cross-check NEXT_PUBLIC_APP_ENV against VERCEL_ENV so prod credentials
// never silently write into a dev keyspace (and vice-versa). Skipped
// locally (no VERCEL_ENV) where the developer owns their .env.local.
if (!env.VERCEL_ENV) return;
const expected = VERCEL_TO_APP_ENV[env.VERCEL_ENV];
if (clientEnv.NEXT_PUBLIC_APP_ENV !== expected) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["NEXT_PUBLIC_APP_ENV"],
message: `VERCEL_ENV=${env.VERCEL_ENV} requires NEXT_PUBLIC_APP_ENV=${expected}, got "${clientEnv.NEXT_PUBLIC_APP_ENV}". Set NEXT_PUBLIC_APP_ENV in Vercel project settings for this environment.`,
});
}
});
const parsed = serverSchema.safeParse(process.env);
if (!parsed.success) {
const issues = Object.entries(parsed.error.flatten().fieldErrors)
.map(([k, v]) => ` ${k}: ${(v ?? []).join(", ")}`)
.join("\n");
throw new Error(`Invalid server environment variables:\n${issues}`);
}
export const serverEnv = {
...parsed.data,
...clientEnv,
};
export { APP_SLUG, SUPABASE_SCHEMA };
export const redisKeyPrefix = `${APP_SLUG}:${clientEnv.NEXT_PUBLIC_APP_ENV}` as const;