diff --git a/apps/editor/env.mjs b/apps/editor/env.mjs new file mode 100644 index 00000000..141ce000 --- /dev/null +++ b/apps/editor/env.mjs @@ -0,0 +1,58 @@ +/** + * Environment variable validation for the editor app. + * + * This file validates that required environment variables are set at runtime. + * Variables are defined in the root .env file. + * + * @see https://env.t3.gg/docs/nextjs + */ +import { createEnv } from '@t3-oss/env-nextjs' +import { z } from 'zod' + +export const env = createEnv({ + /** + * Server-side environment variables (not exposed to client) + */ + server: { + // Database + POSTGRES_URL: z.string().min(1), + SUPABASE_SERVICE_ROLE_KEY: z.string().min(1), + + // Auth + BETTER_AUTH_SECRET: z.string().min(1), + GOOGLE_CLIENT_ID: z.string().optional(), + GOOGLE_CLIENT_SECRET: z.string().optional(), + + // Email + RESEND_API_KEY: z.string().optional(), + }, + + /** + * Client-side environment variables (exposed to browser via NEXT_PUBLIC_) + */ + client: { + NEXT_PUBLIC_SUPABASE_URL: z.string().min(1), + NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().optional(), + }, + + /** + * Runtime values - pulls from process.env + */ + runtimeEnv: { + // Server + POSTGRES_URL: process.env.POSTGRES_URL, + SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY, + BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET, + GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, + RESEND_API_KEY: process.env.RESEND_API_KEY, + // Client + NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, + NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, + }, + + /** + * Skip validation during build (env vars come from Vercel at runtime) + */ + skipValidation: !!process.env.SKIP_ENV_VALIDATION, +}) diff --git a/apps/editor/lib/auth.ts b/apps/editor/lib/auth.ts index c35bb97f..b96fa568 100644 --- a/apps/editor/lib/auth.ts +++ b/apps/editor/lib/auth.ts @@ -1,19 +1,19 @@ import { createAuth } from '@pascal-app/auth/server' import { db } from '@pascal-app/db' import { Resend } from 'resend' +import { env } from '@/env.mjs' import { BASE_URL } from './utils' // Initialize Resend only if API key is available -const resendApiKey = process.env.RESEND_API_KEY -const resend = resendApiKey && resendApiKey.trim() !== '' ? new Resend(resendApiKey) : null +const resend = env.RESEND_API_KEY ? new Resend(env.RESEND_API_KEY) : null export const auth = createAuth({ db, appName: 'Pascal Editor', baseURL: BASE_URL, - secret: process.env.BETTER_AUTH_SECRET || 'build-placeholder-not-used-at-runtime', - googleClientId: process.env.GOOGLE_CLIENT_ID, - googleClientSecret: process.env.GOOGLE_CLIENT_SECRET, + secret: env.BETTER_AUTH_SECRET, + googleClientId: env.GOOGLE_CLIENT_ID, + googleClientSecret: env.GOOGLE_CLIENT_SECRET, sendMagicLink: async ({ email, url }) => { if (!resend) { console.log(`[DEV] Magic link for ${email}: ${url}`) diff --git a/apps/editor/lib/supabase/server.ts b/apps/editor/lib/supabase/server.ts index 2bdfaa22..2e2b072c 100644 --- a/apps/editor/lib/supabase/server.ts +++ b/apps/editor/lib/supabase/server.ts @@ -1,8 +1,6 @@ import { createClient } from '@supabase/supabase-js' import type { SupabaseDatabase } from '@pascal-app/db' - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://placeholder.supabase.co' -const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'placeholder' +import { env } from '@/env.mjs' /** * Supabase client for server-side use with service role key @@ -10,8 +8,8 @@ const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'placeho * Always filter by user_id to enforce permissions */ export const supabaseAdmin = createClient( - supabaseUrl, - supabaseServiceRoleKey, + env.NEXT_PUBLIC_SUPABASE_URL, + env.SUPABASE_SERVICE_ROLE_KEY, { auth: { persistSession: false, diff --git a/apps/editor/package.json b/apps/editor/package.json index afd85909..4f2a6ad2 100644 --- a/apps/editor/package.json +++ b/apps/editor/package.json @@ -31,6 +31,7 @@ "@react-three/uikit-lucide": "^1.0.60", "@repo/ui": "*", "@supabase/supabase-js": "^2.95.3", + "@t3-oss/env-nextjs": "^0.13.10", "@tailwindcss/postcss": "^4.1.18", "@types/three": "^0.182.0", "@vercel/analytics": "^1.6.1", @@ -48,7 +49,8 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "tailwind-merge": "^3.4.0", - "tailwindcss": "^4.1.18" + "tailwindcss": "^4.1.18", + "zod": "^4.3.6" }, "devDependencies": { "@repo/typescript-config": "*", diff --git a/bun.lock b/bun.lock index ce757283..9ab08f85 100644 --- a/bun.lock +++ b/bun.lock @@ -44,6 +44,7 @@ "@react-three/uikit-lucide": "^1.0.60", "@repo/ui": "*", "@supabase/supabase-js": "^2.95.3", + "@t3-oss/env-nextjs": "^0.13.10", "@tailwindcss/postcss": "^4.1.18", "@types/three": "^0.182.0", "@vercel/analytics": "^1.6.1", @@ -62,6 +63,7 @@ "react-dom": "^19.2.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", + "zod": "^4.3.6", }, "devDependencies": { "@repo/typescript-config": "*", @@ -553,6 +555,10 @@ "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + "@t3-oss/env-core": ["@t3-oss/env-core@0.13.10", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g=="], + + "@t3-oss/env-nextjs": ["@t3-oss/env-nextjs@0.13.10", "", { "dependencies": { "@t3-oss/env-core": "0.13.10" }, "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-JfSA2WXOnvcc/uMdp31paMsfbYhhdvLLRxlwvrnlPE9bwM/n0Z+Qb9xRv48nPpvfMhOrkrTYw1I5Yc06WIKBJQ=="], + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], @@ -1559,12 +1565,14 @@ "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zundo": ["zundo@2.3.0", "", { "peerDependencies": { "zustand": "^4.3.0 || ^5.0.0" } }, "sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ=="], "zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="], + "@better-auth/core/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -1631,6 +1639,8 @@ "@vercel/toolbar/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "better-auth/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], diff --git a/turbo.json b/turbo.json index 3ad80519..256dbb71 100644 --- a/turbo.json +++ b/turbo.json @@ -7,6 +7,7 @@ "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": [".next/**", "!.next/cache/**", "dist/**"], "env": [ + "SKIP_ENV_VALIDATION", "BETTER_AUTH_SECRET", "BETTER_AUTH_URL", "GOOGLE_CLIENT_ID",