-
Notifications
You must be signed in to change notification settings - Fork 7
feat: env validation #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: env validation #140
Conversation
WalkthroughConverts Next.js config to a phase-aware function that conditionally loads server/client env modules via a JITI loader, adds typed server/client env modules using @t3-oss/env-nextjs, and replaces direct process.env reads across the codebase with the new Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Runtime as Next.js Runtime
participant NC as next.config.mjs
participant JI as jiti loader
participant ES as src/env/server.ts
participant EC as src/env/client.ts
Runtime->>NC: Load next.config (phase)
NC->>JI: createJiti(import.meta.url)
NC->>JI: conditionally import ES and EC (unless SKIP_ENV_VALIDATION)
JI->>ES: execute server env module
ES->>ES: createEnv(server) -> validate required vars (throws on failure)
JI->>EC: execute client env module
EC->>EC: createEnv(client)
NC-->>Runtime: return NextConfig ({} or augmented)
note right of ES: Validation errors fail startup
sequenceDiagram
autonumber
actor User
participant Browser as Next.js App (client)
participant Server as Next.js Server
participant ENVc as src/env/client.ts
participant ENVs as src/env/server.ts
participant API as app/api/webhook
participant Lib as server libs / routers
User->>Browser: Request page
Browser->>ENVc: read NEXT_PUBLIC_* via env
User->>Server: invoke API (webhook / payment / invoice)
Server->>ENVs: read secrets, NODE_ENV, VERCEL_URL, PORT
Server->>API: validate webhook using ENVs.WEBHOOK_SECRET
Server->>Lib: use ENVs.REQUEST_API_URL / REQUEST_API_KEY / REDIS_URL / ENCRYPTION_KEY
Lib-->>Server: operation results
Server-->>User: response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/api/webhook/route.ts (1)
94-107: Use constant‑time signature comparison and handle missing/invalid header.String inequality leaks timing info and doesn’t guard malformed hex.
- const expectedSignature = crypto - .createHmac("sha256", webhookSecret) - .update(JSON.stringify(body)) - .digest("hex"); - - if (signature !== expectedSignature) { + const payload = JSON.stringify(body); + const expected = crypto.createHmac("sha256", webhookSecret).update(payload).digest(); + const received = + typeof signature === "string" ? Buffer.from(signature, "hex") : null; + if (!received || received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) { return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); }
🧹 Nitpick comments (14)
src/env/server.ts (1)
4-4: CI is failing due to missing required env (fail‑fast is working). Provide CI vars or opt‑out for lint.Given the pipeline error, either inject dummy values in CI for build, or set SKIP_ENV_VALIDATION=true only for lint/typecheck. Example (GitHub Actions):
# Lint job (no secrets needed) env: SKIP_ENV_VALIDATION: 'true' # Build job (dummy non‑secret values OK for type/shape) env: ENCRYPTION_KEY: x DATABASE_URL: postgres://user:pass@localhost:5432/db GOOGLE_CLIENT_ID: x GOOGLE_CLIENT_SECRET: x GOOGLE_REDIRECT_URI: https://example.com/callback CURRENT_ENCRYPTION_VERSION: v1 REQUEST_API_URL: https://api.request.network REQUEST_API_KEY: x WEBHOOK_SECRET: xsrc/server/db/schema.ts (2)
18-18: Drop the redundant cast.env.ENCRYPTION_KEY is typed; the “as string” cast isn’t needed.
-const encryptionKey = env.ENCRYPTION_KEY as string; +const encryptionKey = env.ENCRYPTION_KEY;
104-108: Use validated env for the version (and align types).Avoid reaching into process.env directly; use the validated value.
- const currentVersion = process.env - .CURRENT_ENCRYPTION_VERSION as EncryptionVersion; + const currentVersion = env.CURRENT_ENCRYPTION_VERSION as EncryptionVersion;If you adopt z.enum(["v1"]) in the schema, the cast can be dropped.
src/server/auth/index.ts (1)
111-114: Remove unnecessary casts.env.* entries are already typed as strings here.
-export const google = new Google( - env.GOOGLE_CLIENT_ID as string, - env.GOOGLE_CLIENT_SECRET as string, - env.GOOGLE_REDIRECT_URI as string, -); +export const google = new Google( + env.GOOGLE_CLIENT_ID, + env.GOOGLE_CLIENT_SECRET, + env.GOOGLE_REDIRECT_URI, +);src/server/db/index.ts (1)
8-9: Drop the cast; value is guaranteed by schema.env.DATABASE_URL is already a string.
- connectionString: env.DATABASE_URL as string, + connectionString: env.DATABASE_URL,src/lib/redis/index.ts (1)
8-10: LGTM; consider relying on schema optionality instead of empty-string default.With emptyStringAsUndefined and REDIS_URL optional/url‑validated, the fallback here remains correct.
src/lib/encryption/index.ts (1)
9-10: Tighten typing; remove cast and assert key map.This keeps keys type‑safe against EncryptionVersion.
-const ENCRYPTION_KEYS = { - v1: env.ENCRYPTION_KEY as string, -}; +const ENCRYPTION_KEYS = { + v1: env.ENCRYPTION_KEY, +} as const satisfies Record<EncryptionVersion, string>;src/server/routers/invoice.ts (1)
509-511: Parse and validate TTL as positive integer for Redis SETEX.Number() accepts floats and “0” evaluates falsy. Ensure a strict, positive integer TTL.
Apply this diff:
- Number(env.INVOICE_PROCESSING_TTL) || 60, + (() => { + const ttl = Number.parseInt(env.INVOICE_PROCESSING_TTL, 10); + return Number.isFinite(ttl) && ttl > 0 ? ttl : 60; + })(),src/app/page.tsx (1)
56-56: Drop unnecessary type assertion.env.NEXT_PUBLIC_API_TERMS_CONDITIONS is already typed as string by the client env schema.
Apply this diff:
- href={env.NEXT_PUBLIC_API_TERMS_CONDITIONS as string} + href={env.NEXT_PUBLIC_API_TERMS_CONDITIONS}src/app/layout.tsx (1)
37-37: Render GTM only when an ID is provided.Prevents injecting a script with an unintended “undefined” id in some setups. Prior learning allows assertion; consider this as a low‑prio guard.
Apply this diff:
- <GoogleTagManager gtmId={env.NEXT_PUBLIC_GTM_ID as string} /> + {env.NEXT_PUBLIC_GTM_ID ? ( + <GoogleTagManager gtmId={env.NEXT_PUBLIC_GTM_ID} /> + ) : null}src/components/compliance-form.tsx (1)
71-73: Trim and de-duplicate trusted origins.Improves robustness when comma-separated values contain spaces or accidental empty entries.
Apply this diff:
- const origins = env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS - ? env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS.split(",") - : ["https://request.network", "https://core-api-staging.pay.so"]; + const origins = (env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS + ? env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS.split(",") + : ["https://request.network", "https://core-api-staging.pay.so"] + ) + .map((o) => o.trim()) + .filter(Boolean);src/env/client.ts (3)
7-7: Require HTTPS for Terms & Conditions URL.Prevents accidentally using non‑TLS links.
- NEXT_PUBLIC_API_TERMS_CONDITIONS: z.string().url(), + NEXT_PUBLIC_API_TERMS_CONDITIONS: z + .string() + .url() + .refine((u) => u.startsWith("https://"), "HTTPS URL required"),
4-19: Consider emptyStringAsUndefined to catch “set but empty” configs.Helps fail fast when variables are present but blank.
export const env = createEnv({ client: { ... }, - experimental__runtimeEnv: process.env, + experimental__runtimeEnv: process.env, + emptyStringAsUndefined: true, });
9-9: Validate/normalize NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS (comma-separated origins)Currently defined as z.string().optional() in src/env/client.ts — ensure each comma-separated origin is a valid http(s) URL.
- NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS: z.string().optional(), + NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS: z + .string() + .optional() + .refine( + (s) => + !s || + s + .split(",") + .map((x) => x.trim()) + .every((o) => { + try { + const u = new URL(o); + return u.protocol === "https:" || u.protocol === "http:"; + } catch { + return false; + } + }), + "Comma-separated origins; each must be a valid http(s) URL" + ),If you prefer returning an array instead (breaking change), I can provide a transform diff.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (17)
next.config.mjs(1 hunks)package.json(3 hunks)src/app/api/webhook/route.ts(2 hunks)src/app/layout.tsx(2 hunks)src/app/page.tsx(2 hunks)src/components/app-kit.tsx(2 hunks)src/components/compliance-form.tsx(2 hunks)src/env/client.ts(1 hunks)src/env/server.ts(1 hunks)src/lib/axios.ts(1 hunks)src/lib/encryption/index.ts(1 hunks)src/lib/redis/index.ts(1 hunks)src/server/auth/index.ts(2 hunks)src/server/db/index.ts(1 hunks)src/server/db/schema.ts(2 hunks)src/server/routers/invoice.ts(4 hunks)src/server/routers/payment.ts(3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-02-12T12:40:14.742Z
Learnt from: aimensahnoun
PR: RequestNetwork/easy-invoice#2
File: src/server/routers/invoice.ts:88-109
Timestamp: 2025-02-12T12:40:14.742Z
Learning: The payRequest endpoint in src/server/routers/invoice.ts is intentionally kept public (using publicProcedure) to allow invoice sharing and payment by anyone with the payment reference, similar to how payment links work in other payment systems.
Applied to files:
src/server/routers/invoice.ts
📚 Learning: 2025-08-26T22:05:39.051Z
Learnt from: MantisClone
PR: RequestNetwork/easy-invoice#133
File: src/server/routers/invoice.ts:483-514
Timestamp: 2025-08-26T22:05:39.051Z
Learning: In the RequestNetwork/easy-invoice codebase, for the setInvoiceAsProcessing Redis implementation, the team chose setex over SET NX EX because the race condition risk is minimal in their context due to single user access patterns, UI-level payment button disabling, narrow timing windows, and existing TTL cleanup mechanisms. The simpler setex approach is preferred over the more complex distributed lock pattern when the practical risk is negligible.
Applied to files:
src/server/routers/invoice.ts
📚 Learning: 2025-02-19T14:23:43.558Z
Learnt from: aimensahnoun
PR: RequestNetwork/easy-invoice#20
File: src/app/layout.tsx:34-34
Timestamp: 2025-02-19T14:23:43.558Z
Learning: In the Easy Invoice project, type assertion for the NEXT_PUBLIC_GTM_ID environment variable in Google Tag Manager integration is acceptable without additional runtime validation.
Applied to files:
src/app/layout.tsx
🧬 Code graph analysis (15)
src/app/page.tsx (1)
src/env/client.ts (1)
env(4-19)
src/lib/encryption/index.ts (1)
src/env/server.ts (1)
env(4-21)
src/app/api/webhook/route.ts (1)
src/env/server.ts (1)
env(4-21)
src/server/db/schema.ts (1)
src/env/server.ts (1)
env(4-21)
src/server/db/index.ts (1)
src/env/server.ts (1)
env(4-21)
src/components/app-kit.tsx (1)
src/env/client.ts (1)
env(4-19)
src/server/routers/invoice.ts (1)
src/env/server.ts (1)
env(4-21)
src/server/auth/index.ts (1)
src/env/server.ts (1)
env(4-21)
src/app/layout.tsx (1)
src/env/client.ts (1)
env(4-19)
src/lib/axios.ts (1)
src/env/server.ts (1)
env(4-21)
src/server/routers/payment.ts (1)
src/env/server.ts (1)
env(4-21)
src/components/compliance-form.tsx (1)
src/env/client.ts (1)
env(4-19)
src/env/server.ts (1)
src/env/client.ts (1)
env(4-19)
src/env/client.ts (1)
src/env/server.ts (1)
env(4-21)
src/lib/redis/index.ts (1)
src/env/server.ts (1)
env(4-21)
🪛 GitHub Actions: Build and Lint
next.config.mjs
[error] 5-5: Next.js config load failed due to invalid environment variables. Build aborted by env validation. Required env vars undefined: ENCRYPTION_KEY, DATABASE_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI, CURRENT_ENCRYPTION_VERSION, REQUEST_API_URL, REQUEST_API_KEY, WEBHOOK_SECRET. Command: npm run build.
src/env/server.ts
[error] 4-4: Invalid environment variables: 9 required string values were undefined during environment creation (ENCRYPTION_KEY, DATABASE_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI, CURRENT_ENCRYPTION_VERSION, REQUEST_API_URL, REQUEST_API_KEY, WEBHOOK_SECRET).
🔇 Additional comments (5)
package.json (2)
68-68: Zod upgrade LGTM.No breaking code paths here; aligns with env validation usage.
37-37: Env lib addition looks good — TS >=5 & bundler moduleResolution verifiedtsconfig.json contains "moduleResolution": "bundler" and "module": "esnext"; package-lock.json records typescript@5.7.3 (>=5).
src/server/routers/payment.ts (1)
23-25: LGTM: centralized env usage for fee config.Matches the new env pattern and keeps server-side sourcing.
src/components/app-kit.tsx (1)
22-26: LGTM: client env adoption with defensive check.Project ID pulled from typed env; runtime guard is acceptable.
src/env/client.ts (1)
11-18: Verify Next.js version — switch to experimental__runtimeEnv only if Next.js ≥ 13.4.4.File: src/env/client.ts (lines 11–18)
- runtimeEnv: { - NEXT_PUBLIC_REOWN_PROJECT_ID: process.env.NEXT_PUBLIC_REOWN_PROJECT_ID, - NEXT_PUBLIC_API_TERMS_CONDITIONS: - process.env.NEXT_PUBLIC_API_TERMS_CONDITIONS, - NEXT_PUBLIC_GTM_ID: process.env.NEXT_PUBLIC_GTM_ID, - NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS: - process.env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS, - }, + experimental__runtimeEnv: process.env,Confirm Next.js version (run
npm ls nextor show package.json) and apply the diff only if version ≥ 13.4.4; otherwise keep the explicit runtimeEnv mapping.
| "@radix-ui/react-tooltip": "^1.2.8", | ||
| "@reown/appkit": "^1.6.8", | ||
| "@reown/appkit-adapter-ethers5": "^1.6.8", | ||
| "@t3-oss/env-nextjs": "^0.13.8", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Search for remaining process.env usages and update to env.*.
To ensure completeness of the migration, scan for direct process.env access (excluding env modules and config).
Also applies to: 53-53
🏁 Script executed:
#!/bin/bash
# Find raw process.env usages outside env modules and config
rg -nP --type=ts --type=tsx -C2 '(?<!env\/)(?<!next\.config)\bprocess\.env\.[A-Z0-9_]+' -g '!**/src/env/**' -g '!**/next.config.*'Length of output: 68
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Searching for raw process.env usages in ts/tsx/js/jsx files (excluding src/env and next.config.*)..."
rg -n -S 'process\.env\.' -g '!src/env/**' -g '!**/next.config.*' -g '!node_modules/**' -g '!dist/**' -g '!build/**' -g '!**/.git/**' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' -C2 || true
echo "Done."Length of output: 4167
Replace remaining direct process.env usages with env. (client vs server as appropriate).*
Found raw process.env references that must be migrated (ignore src/env/* — that’s the env module):
- src/trpc/shared.ts:10-11 — VERCEL_URL, PORT
- src/trpc/server.ts:36 — NODE_ENV
- src/trpc/react.tsx:37 — NODE_ENV
- src/middleware.ts:17 — NODE_ENV
- src/server/auth/index.ts:80,91 — NODE_ENV
- src/components/header.tsx:10 — NEXT_PUBLIC_DEMO_MEETING
- src/components/compliance-form.tsx:76 — NODE_ENV
- src/app/login/google/route.ts:20,27 — NODE_ENV
- src/app/api/trpc/[trpc]/route.ts:63 — NODE_ENV
- drizzle.config.ts:8,10 — DATABASE_URL
Update public vars to the env client accessor (env.NEXT_PUBLIC_…), and server-only vars (DATABASE_URL, PORT, NODE_ENV, VERCEL_URL) to the server env accessor from your env module. Re-run a repo-wide search (excluding src/env/** and next.config.*) to confirm no raw process.env remain.
🤖 Prompt for AI Agents
package.json lines 37-37: Several raw process.env usages remain across the repo
and must be replaced with the project's env accessors; update public variables
to use the client accessor (env.NEXT_PUBLIC_...) and server-only variables to
use the server env accessor from src/env (e.g., env.DATABASE_URL, env.PORT,
env.NODE_ENV, env.VERCEL_URL), changing the specific occurrences noted
(src/trpc/shared.ts:10-11, src/trpc/server.ts:36, src/trpc/react.tsx:37,
src/middleware.ts:17, src/server/auth/index.ts:80,91,
src/components/header.tsx:10, src/components/compliance-form.tsx:76,
src/app/login/google/route.ts:20,27, src/app/api/trpc/[trpc]/route.ts:63,
drizzle.config.ts:8,10), then run a repo-wide search (excluding src/env/** and
next.config.*) to ensure no raw process.env references remain.
| import { createEnv } from "@t3-oss/env-nextjs"; | ||
| import { z } from "zod"; | ||
|
|
||
| export const env = createEnv({ | ||
| client: { | ||
| NEXT_PUBLIC_REOWN_PROJECT_ID: z.string().min(1), | ||
| NEXT_PUBLIC_API_TERMS_CONDITIONS: z.string().url(), | ||
| NEXT_PUBLIC_GTM_ID: z.string().optional(), | ||
| NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS: z.string().optional(), | ||
| }, | ||
| runtimeEnv: { | ||
| NEXT_PUBLIC_REOWN_PROJECT_ID: process.env.NEXT_PUBLIC_REOWN_PROJECT_ID, | ||
| NEXT_PUBLIC_API_TERMS_CONDITIONS: | ||
| process.env.NEXT_PUBLIC_API_TERMS_CONDITIONS, | ||
| NEXT_PUBLIC_GTM_ID: process.env.NEXT_PUBLIC_GTM_ID, | ||
| NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS: | ||
| process.env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify client env isn’t imported in server‑only code.
Sanity‑check that "@/env/client" is used only in client components/pages.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "All imports of '@/env/client':"
rg -n -C2 -P 'from\s+[\'"]@/env/client[\'"]' || true
echo
echo "Potential server contexts importing client env (should be none):"
rg -n -C2 -P 'from\s+[\'"]@/env/client[\'"]' \
| rg -nP '(?:^|/)(app/.*/route\.(ts|tsx|js)|pages/api/|middleware\.(ts|js)|src/server/|/lib/server/)'Length of output: 92
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "All occurrences of '@/env/client' (with 2 lines of context):"
rg -n -S "@/env/client" -C2 || true
echo
echo "Potential server contexts importing client env (should be none):"
# list unique file paths that reference "@/env/client"
rg -n -S "@/env/client" || true \
| cut -d: -f1 \
| sort -u \
| while read -r p; do
case "$p" in
pages/api/*|*/pages/api/*|src/server/*|*/src/server/*|*/lib/server/*|lib/server/*|middleware.ts|middleware.js)
echo "$p"
;;
app/*|*/app/*)
case "$p" in
*route.ts|*route.tsx|*route.js) echo "$p" ;;
esac
;;
esac
doneLength of output: 1559
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "All files importing '@/env/client':"
rg -n -S "@/env/client" -C2 || true
echo
echo "Inspecting each file's header (first 120 lines) and whether it contains a 'use client' directive:"
rg -n -S -l "@/env/client" || true \
| sort -u \
| while read -r f; do
echo
echo "=== $f ==="
if [ -f "$f" ]; then
sed -n '1,120p' "$f" | nl -ba -v1 -w3 -s': '
echo "---- 'use client' lines (if any) ----"
rg -n -S -e '^"use client"' -e "^'use client'" "$f" || true
else
echo "MISSING: $f"
fi
doneLength of output: 1413
Remove client-only env imports from server components / ensure imports live in client components
Imports of "@/env/client" were found in:
- src/app/layout.tsx
- src/app/page.tsx
- src/components/compliance-form.tsx
- src/components/app-kit.tsx (contains "use client" — OK)
Action: For files that must remain server (layout/page/compliance-form) remove the "@/env/client" import and either use a server-safe env access or move the client-dependent logic into a true client component; for files that should be client, add "use client" at the top and keep the import.
🤖 Prompt for AI Agents
In src/env/client.ts lines 1-19: the client-only env exports are being imported
into server components; either stop importing this file from server components
by creating a server-safe env module (e.g., src/env/server.ts) that reads
required vars from process.env and exporting only server-appropriate keys for
layout/page/compliance-form, or move any client-dependent logic in those files
into true client components and add "use client" at the top of those files;
ensure client components continue to import src/env/client.ts and server
components import the new server-safe module so runtime access is safe.
| client: { | ||
| NEXT_PUBLIC_REOWN_PROJECT_ID: z.string().min(1), | ||
| NEXT_PUBLIC_API_TERMS_CONDITIONS: z.string().url(), | ||
| NEXT_PUBLIC_GTM_ID: z.string().optional(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Tighten GTM ID validation.
Reduce silent misconfig by validating the GTM container ID shape.
- NEXT_PUBLIC_GTM_ID: z.string().optional(),
+ NEXT_PUBLIC_GTM_ID: z
+ .string()
+ .regex(/^GTM-[A-Z0-9]+$/i, "Expected GTM container ID like GTM-XXXXXXX")
+ .optional(),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| NEXT_PUBLIC_GTM_ID: z.string().optional(), | |
| NEXT_PUBLIC_GTM_ID: z | |
| .string() | |
| .regex(/^GTM-[A-Z0-9]+$/i, "Expected GTM container ID like GTM-XXXXXXX") | |
| .optional(), |
🤖 Prompt for AI Agents
In src/env/client.ts around line 8, the NEXT_PUBLIC_GTM_ID schema uses
z.string().optional() which allows silent misconfigs; tighten validation by
requiring the GTM container ID to match the expected pattern (e.g.
/^GTM-[A-Z0-9]+$/) and optionally trim/uppercase input before validation;
replace the current schema with a regex-based string check
(z.string().transform(...).regex(...).optional() or
z.string().regex(...).optional()) so only valid GTM IDs are accepted and invalid
values are rejected.
| export const env = createEnv({ | ||
| server: { | ||
| ENCRYPTION_KEY: z.string().min(1), | ||
| DATABASE_URL: z.string().min(1), | ||
| GOOGLE_CLIENT_ID: z.string().min(1), | ||
| GOOGLE_CLIENT_SECRET: z.string().min(1), | ||
| GOOGLE_REDIRECT_URI: z.string().min(1), | ||
| CURRENT_ENCRYPTION_VERSION: z.string().min(1), | ||
| REQUEST_API_URL: z.string().url(), | ||
| REQUEST_API_KEY: z.string().min(1), | ||
| WEBHOOK_SECRET: z.string().min(1), | ||
| FEE_PERCENTAGE_FOR_PAYMENT: z.string().default(""), | ||
| FEE_ADDRESS_FOR_PAYMENT: z.string().default(""), | ||
| REDIS_URL: z.string().default(""), | ||
| INVOICE_PROCESSING_TTL: z.string().default(""), | ||
| }, | ||
| experimental__runtimeEnv: process.env, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix CI breakage: gate validation for lint/build-only jobs; tighten schema.
The current setup validates at import-time and fails CI when secrets aren’t provided (see pipeline error). Add skipValidation for non-runtime tasks and treat empty strings as undefined. Also tighten a few schemas and coerce numeric vars.
Apply this diff:
export const env = createEnv({
server: {
- ENCRYPTION_KEY: z.string().min(1),
- DATABASE_URL: z.string().min(1),
+ ENCRYPTION_KEY: z.string().min(1),
+ DATABASE_URL: z.string().min(1),
GOOGLE_CLIENT_ID: z.string().min(1),
GOOGLE_CLIENT_SECRET: z.string().min(1),
- GOOGLE_REDIRECT_URI: z.string().min(1),
- CURRENT_ENCRYPTION_VERSION: z.string().min(1),
+ GOOGLE_REDIRECT_URI: z.string().url(),
+ CURRENT_ENCRYPTION_VERSION: z.enum(["v1"]),
REQUEST_API_URL: z.string().url(),
REQUEST_API_KEY: z.string().min(1),
WEBHOOK_SECRET: z.string().min(1),
- FEE_PERCENTAGE_FOR_PAYMENT: z.string().default(""),
- FEE_ADDRESS_FOR_PAYMENT: z.string().default(""),
- REDIS_URL: z.string().default(""),
- INVOICE_PROCESSING_TTL: z.string().default(""),
+ FEE_PERCENTAGE_FOR_PAYMENT: z.coerce.number().min(0).max(100).optional(),
+ FEE_ADDRESS_FOR_PAYMENT: z.string().optional(),
+ REDIS_URL: z.string().url().optional(),
+ INVOICE_PROCESSING_TTL: z.coerce.number().int().positive().optional(),
},
- experimental__runtimeEnv: process.env,
+ // For Next.js >=13.4.4 this is the recommended pattern
+ experimental__runtimeEnv: process.env,
+ // CI/lint/typecheck can opt-out of validation (fail-fast remains for dev/start)
+ skipValidation: !!process.env.SKIP_ENV_VALIDATION,
+ // Make empty values behave like "unset"
+ emptyStringAsUndefined: true,
});- Docs for experimental__runtimeEnv (Next ≥ 13.4.4). (env.t3.gg)
- Docs for skipValidation and emptyStringAsUndefined. (env.t3.gg)
Operational follow-up: set SKIP_ENV_VALIDATION=true only for lint/typecheck steps; keep it false for next dev/start.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const env = createEnv({ | |
| server: { | |
| ENCRYPTION_KEY: z.string().min(1), | |
| DATABASE_URL: z.string().min(1), | |
| GOOGLE_CLIENT_ID: z.string().min(1), | |
| GOOGLE_CLIENT_SECRET: z.string().min(1), | |
| GOOGLE_REDIRECT_URI: z.string().min(1), | |
| CURRENT_ENCRYPTION_VERSION: z.string().min(1), | |
| REQUEST_API_URL: z.string().url(), | |
| REQUEST_API_KEY: z.string().min(1), | |
| WEBHOOK_SECRET: z.string().min(1), | |
| FEE_PERCENTAGE_FOR_PAYMENT: z.string().default(""), | |
| FEE_ADDRESS_FOR_PAYMENT: z.string().default(""), | |
| REDIS_URL: z.string().default(""), | |
| INVOICE_PROCESSING_TTL: z.string().default(""), | |
| }, | |
| experimental__runtimeEnv: process.env, | |
| }); | |
| export const env = createEnv({ | |
| server: { | |
| ENCRYPTION_KEY: z.string().min(1), | |
| DATABASE_URL: z.string().min(1), | |
| GOOGLE_CLIENT_ID: z.string().min(1), | |
| GOOGLE_CLIENT_SECRET: z.string().min(1), | |
| GOOGLE_REDIRECT_URI: z.string().url(), | |
| CURRENT_ENCRYPTION_VERSION: z.enum(["v1"]), | |
| REQUEST_API_URL: z.string().url(), | |
| REQUEST_API_KEY: z.string().min(1), | |
| WEBHOOK_SECRET: z.string().min(1), | |
| FEE_PERCENTAGE_FOR_PAYMENT: z.coerce.number().min(0).max(100).optional(), | |
| FEE_ADDRESS_FOR_PAYMENT: z.string().optional(), | |
| REDIS_URL: z.string().url().optional(), | |
| INVOICE_PROCESSING_TTL: z.coerce.number().int().positive().optional(), | |
| }, | |
| // For Next.js >=13.4.4 this is the recommended pattern | |
| experimental__runtimeEnv: process.env, | |
| // CI/lint/typecheck can opt-out of validation (fail-fast remains for dev/start) | |
| skipValidation: !!process.env.SKIP_ENV_VALIDATION, | |
| // Make empty values behave like "unset" | |
| emptyStringAsUndefined: true, | |
| }); |
🧰 Tools
🪛 GitHub Actions: Build and Lint
[error] 4-4: Invalid environment variables: 9 required string values were undefined during environment creation (ENCRYPTION_KEY, DATABASE_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI, CURRENT_ENCRYPTION_VERSION, REQUEST_API_URL, REQUEST_API_KEY, WEBHOOK_SECRET).
| baseURL: env.REQUEST_API_URL, | ||
| headers: { | ||
| "x-api-key": env.REQUEST_API_KEY, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a client-wide timeout for outbound API calls.
External calls without timeouts can hang threads and degrade reliability.
Apply this diff:
export const apiClient = axios.create({
baseURL: env.REQUEST_API_URL,
headers: {
"x-api-key": env.REQUEST_API_KEY,
},
+ timeout: 10000, // 10s fail-fast
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| baseURL: env.REQUEST_API_URL, | |
| headers: { | |
| "x-api-key": env.REQUEST_API_KEY, | |
| }, | |
| }); | |
| export const apiClient = axios.create({ | |
| baseURL: env.REQUEST_API_URL, | |
| headers: { | |
| "x-api-key": env.REQUEST_API_KEY, | |
| }, | |
| timeout: 10000, // 10s fail-fast | |
| }); |
🤖 Prompt for AI Agents
In src/lib/axios.ts around lines 5 to 9, the axios instance is created without a
request timeout; add a client-wide timeout to prevent hanging external calls by
adding a timeout property to the axios.create config (use a configurable value,
e.g. parseInt(env.REQUEST_API_TIMEOUT) with a safe default like 5000 ms if the
env var is missing/invalid) so the instance includes baseURL, headers, and
timeout in its config.
| if (env.FEE_PERCENTAGE_FOR_PAYMENT) { | ||
| params.append("feePercentage", env.FEE_PERCENTAGE_FOR_PAYMENT); | ||
| } | ||
| if (process.env.FEE_ADDRESS_FOR_PAYMENT) { | ||
| params.append("feeAddress", process.env.FEE_ADDRESS_FOR_PAYMENT); | ||
| if (env.FEE_ADDRESS_FOR_PAYMENT) { | ||
| params.append("feeAddress", env.FEE_ADDRESS_FOR_PAYMENT); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Include fee params atomically (only when both are present).
Currently, you may send only one of the two fee params. Align with payment.ts behavior to avoid partial fee config hitting the API.
Apply this diff:
- if (env.FEE_PERCENTAGE_FOR_PAYMENT) {
- params.append("feePercentage", env.FEE_PERCENTAGE_FOR_PAYMENT);
- }
- if (env.FEE_ADDRESS_FOR_PAYMENT) {
- params.append("feeAddress", env.FEE_ADDRESS_FOR_PAYMENT);
- }
+ const feePct = env.FEE_PERCENTAGE_FOR_PAYMENT;
+ const feeAddr = env.FEE_ADDRESS_FOR_PAYMENT;
+ if (feePct && feeAddr) {
+ params.append("feePercentage", feePct);
+ params.append("feeAddress", feeAddr);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (env.FEE_PERCENTAGE_FOR_PAYMENT) { | |
| params.append("feePercentage", env.FEE_PERCENTAGE_FOR_PAYMENT); | |
| } | |
| if (process.env.FEE_ADDRESS_FOR_PAYMENT) { | |
| params.append("feeAddress", process.env.FEE_ADDRESS_FOR_PAYMENT); | |
| if (env.FEE_ADDRESS_FOR_PAYMENT) { | |
| params.append("feeAddress", env.FEE_ADDRESS_FOR_PAYMENT); | |
| } | |
| const feePct = env.FEE_PERCENTAGE_FOR_PAYMENT; | |
| const feeAddr = env.FEE_ADDRESS_FOR_PAYMENT; | |
| if (feePct && feeAddr) { | |
| params.append("feePercentage", feePct); | |
| params.append("feeAddress", feeAddr); | |
| } |
🤖 Prompt for AI Agents
In src/server/routers/invoice.ts around lines 378 to 383, the current code
appends feePercentage and feeAddress independently which can result in sending
only one of the two fee params; change the logic to append both feePercentage
and feeAddress atomically only when both env.FEE_PERCENTAGE_FOR_PAYMENT and
env.FEE_ADDRESS_FOR_PAYMENT are present (e.g., check both with a single
conditional and then call params.append for both inside that block) so partial
fee configuration is never sent to the API.
| const response = await apiClient.get( | ||
| `/v2/request/${requestId}/routes?wallet=${walletAddress}&feePercentage=${process.env.FEE_PERCENTAGE_FOR_PAYMENT}&feeAddress=${process.env.FEE_ADDRESS_FOR_PAYMENT}`, | ||
| `/v2/request/${requestId}/routes?wallet=${walletAddress}&feePercentage=${env.FEE_PERCENTAGE_FOR_PAYMENT}&feeAddress=${env.FEE_ADDRESS_FOR_PAYMENT}`, | ||
| ); | ||
|
|
||
| if (response.status !== 200) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t send empty fee query params; build query safely.
The route always includes fee params even when they’re empty strings. Build the query with URLSearchParams and include fees only if both exist.
Apply this diff:
- const response = await apiClient.get(
- `/v2/request/${requestId}/routes?wallet=${walletAddress}&feePercentage=${env.FEE_PERCENTAGE_FOR_PAYMENT}&feeAddress=${env.FEE_ADDRESS_FOR_PAYMENT}`,
- );
+ const qs = new URLSearchParams({ wallet: walletAddress });
+ if (env.FEE_PERCENTAGE_FOR_PAYMENT && env.FEE_ADDRESS_FOR_PAYMENT) {
+ qs.set("feePercentage", env.FEE_PERCENTAGE_FOR_PAYMENT);
+ qs.set("feeAddress", env.FEE_ADDRESS_FOR_PAYMENT);
+ }
+ const response = await apiClient.get(
+ `/v2/request/${requestId}/routes?${qs.toString()}`,
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const response = await apiClient.get( | |
| `/v2/request/${requestId}/routes?wallet=${walletAddress}&feePercentage=${process.env.FEE_PERCENTAGE_FOR_PAYMENT}&feeAddress=${process.env.FEE_ADDRESS_FOR_PAYMENT}`, | |
| `/v2/request/${requestId}/routes?wallet=${walletAddress}&feePercentage=${env.FEE_PERCENTAGE_FOR_PAYMENT}&feeAddress=${env.FEE_ADDRESS_FOR_PAYMENT}`, | |
| ); | |
| if (response.status !== 200) { | |
| const qs = new URLSearchParams({ wallet: walletAddress }); | |
| if (env.FEE_PERCENTAGE_FOR_PAYMENT && env.FEE_ADDRESS_FOR_PAYMENT) { | |
| qs.set("feePercentage", env.FEE_PERCENTAGE_FOR_PAYMENT); | |
| qs.set("feeAddress", env.FEE_ADDRESS_FOR_PAYMENT); | |
| } | |
| const response = await apiClient.get( | |
| `/v2/request/${requestId}/routes?${qs.toString()}`, | |
| ); | |
| if (response.status !== 200) { |
🤖 Prompt for AI Agents
In src/server/routers/invoice.ts around lines 451 to 455, the GET request URL is
built with fee query params even when env.FEE_PERCENTAGE_FOR_PAYMENT or
env.FEE_ADDRESS_FOR_PAYMENT are empty; change this to build the query using
URLSearchParams (or similar) so you always include wallet and only append
feePercentage and feeAddress when both env variables are non-empty (or both
truthy). Construct the params object/URLSearchParams, append wallet, then
conditionally append feePercentage and feeAddress only if both exist, and use
params.toString() when calling apiClient.get to avoid sending empty query
parameters.
| feePercentage: env.FEE_PERCENTAGE_FOR_PAYMENT, | ||
| feeAddress: env.FEE_ADDRESS_FOR_PAYMENT, | ||
| })) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gate batch payout fees the same way as single payouts.
You always include fee fields here, which can send empty strings. Mirror the single-pay conditional.
Apply this diff:
- feePercentage: env.FEE_PERCENTAGE_FOR_PAYMENT,
- feeAddress: env.FEE_ADDRESS_FOR_PAYMENT,
+ ...(env.FEE_PERCENTAGE_FOR_PAYMENT &&
+ env.FEE_ADDRESS_FOR_PAYMENT
+ ? {
+ feePercentage: env.FEE_PERCENTAGE_FOR_PAYMENT,
+ feeAddress: env.FEE_ADDRESS_FOR_PAYMENT,
+ }
+ : {}),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| feePercentage: env.FEE_PERCENTAGE_FOR_PAYMENT, | |
| feeAddress: env.FEE_ADDRESS_FOR_PAYMENT, | |
| })) | |
| ...(env.FEE_PERCENTAGE_FOR_PAYMENT && | |
| env.FEE_ADDRESS_FOR_PAYMENT | |
| ? { | |
| feePercentage: env.FEE_PERCENTAGE_FOR_PAYMENT, | |
| feeAddress: env.FEE_ADDRESS_FOR_PAYMENT, | |
| } | |
| : {}), | |
| })) |
🤖 Prompt for AI Agents
In src/server/routers/payment.ts around lines 68-70, the batch payout object
always includes feePercentage and feeAddress (which can be empty strings);
change this to match the single-payout logic by only adding those fields when
both env.FEE_PERCENTAGE_FOR_PAYMENT and env.FEE_ADDRESS_FOR_PAYMENT are present
and non-empty. Implement the gating by conditionally adding the fee fields
(e.g., wrap them in a conditional spread or an if that adds them) so empty
strings are not sent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
.github/workflows/build-and-lint.yml(1 hunks)next.config.mjs(1 hunks)package.json(2 hunks)src/app/api/trpc/[trpc]/route.ts(2 hunks)src/app/login/google/route.ts(2 hunks)src/components/compliance-form.tsx(2 hunks)src/components/header.tsx(1 hunks)src/env/client.ts(1 hunks)src/env/server.ts(1 hunks)src/middleware.ts(2 hunks)src/server/auth/index.ts(4 hunks)src/trpc/react.tsx(2 hunks)src/trpc/server.ts(2 hunks)src/trpc/shared.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- src/env/client.ts
- src/server/auth/index.ts
- src/env/server.ts
- package.json
🧰 Additional context used
🧬 Code graph analysis (8)
src/middleware.ts (1)
src/env/server.ts (1)
env(4-24)
src/trpc/react.tsx (1)
src/env/server.ts (1)
env(4-24)
src/app/login/google/route.ts (1)
src/env/server.ts (1)
env(4-24)
src/app/api/trpc/[trpc]/route.ts (1)
src/env/server.ts (1)
env(4-24)
src/components/header.tsx (2)
src/server/db/schema.ts (1)
User(388-388)src/env/client.ts (1)
env(4-21)
src/trpc/server.ts (1)
src/env/server.ts (1)
env(4-24)
src/trpc/shared.ts (1)
src/env/server.ts (1)
env(4-24)
src/components/compliance-form.tsx (2)
src/env/client.ts (1)
env(4-21)src/env/server.ts (1)
env(4-24)
🔇 Additional comments (6)
src/middleware.ts (1)
4-4: Consistent env access for cookie security — looks good.Also applies to: 18-19
src/app/api/trpc/[trpc]/route.ts (1)
5-5: Use of env for dev‑only error logging — good change.Also applies to: 64-70
src/app/login/google/route.ts (1)
1-1: Secure cookie flag via env — good and consistent.Also applies to: 21-21, 28-28
src/components/header.tsx (1)
3-3: Client env wrapper adoption — looks correct.Also applies to: 11-11
src/trpc/server.ts (1)
14-15: Swapping to env.NODE_ENV in loggerLink — good consistency.Also applies to: 37-39
next.config.mjs (1)
1-6: Gated, phase‑aware env validation — solid, but sandbox verification failedVerification blocked: importing next.config.mjs failed with Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'jiti'. Run the provided script locally with project deps installed (npm/yarn install), or add 'jiti' to package.json (devDependency), or guard/delay the jiti import so automated verification can run.
| env: | ||
| SKIP_ENV_VALIDATION: "1" | ||
| run: npm run build |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Also skip validation during lint to avoid false CI failures.
Some linters/tsc tasks load Next config and would trigger env validation. Mirror SKIP_ENV_VALIDATION for the lint step.
Apply this diff:
- - name: Check formatting and lint
- run: npm run check
+ - name: Check formatting and lint
+ env:
+ SKIP_ENV_VALIDATION: "1"
+ run: npm run check📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| env: | |
| SKIP_ENV_VALIDATION: "1" | |
| run: npm run build | |
| - name: Check formatting and lint | |
| env: | |
| SKIP_ENV_VALIDATION: "1" | |
| run: npm run check |
🤖 Prompt for AI Agents
.github/workflows/build-and-lint.yml around lines 94 to 96: the lint step can
trigger Next config env validation causing false CI failures; add the
SKIP_ENV_VALIDATION: "1" environment variable to the lint job just like the
build job so lint/tsc tasks skip env validation. Update the lint step's env
block to include SKIP_ENV_VALIDATION: "1" (mirror the build step) so both steps
run without triggering env checks.
| import { env } from "@/env/client"; | ||
| import { env as serverEnv } from "@/env/server"; | ||
| import { COMPLIANCE_COUNTRIES } from "@/lib/constants/compliance"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Don’t import server env in a client component; trim/dedupe origins.
Importing "@/env/server" in a "use client" file risks bundling server-only values and can break builds. Also, normalize the comma list and avoid duplicates.
Apply this diff:
-import { env as serverEnv } from "@/env/server";
+// Avoid importing server env in client code
...
const TRUSTED_ORIGINS = useMemo(() => {
- const origins = env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS
- ? env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS.split(",")
- : ["https://request.network", "https://core-api-staging.pay.so"];
+ const originsBase = env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS
+ ? env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS.split(",")
+ : ["https://request.network", "https://core-api-staging.pay.so"];
+ const origins = originsBase.map((s) => s.trim()).filter(Boolean);
// Add localhost in development
- if (serverEnv.NODE_ENV === "development") {
- origins.push("http://localhost:3000");
- }
+ if (process.env.NODE_ENV === "development") {
+ origins.push("http://localhost:3000");
+ }
- return origins;
+ return Array.from(new Set(origins));
}, []);Run to ensure no other client files import the server env:
Also applies to: 72-79
🏁 Script executed:
#!/bin/bash
# Find client components importing "@/env/server"
mapfile -t CLIENTS < <(rg -l -nP --type=tsx '^\s*"use client";')
for f in "${CLIENTS[@]}"; do
rg -n '@/env/server' "$f" || true
doneLength of output: 84
🏁 Script executed:
#!/bin/bash
# Find client components importing "@/env/server" without process substitution or --type
PAT="^\s*['\"]use client['\"]\s*;?"
CLIENT_FILES=$(rg -l -nP "$PAT" -g '**/*.{tsx,jsx}' --color=never || true)
if [ -z "$CLIENT_FILES" ]; then
echo "NO_CLIENT_FILES_FOUND"
exit 0
fi
printf '%s\n' "$CLIENT_FILES" | while IFS= read -r f; do
echo "---- $f ----"
rg -n --color=never '@/env/server' "$f" || echo "No @/env/server import in $f"
doneLength of output: 5499
Remove server env imports from client components; trim/dedupe trusted origins
Found '@/env/server' imported in client files:
- src/components/compliance-form.tsx — line 35
- src/trpc/react.tsx — line 9
Apply the diff below to compliance-form.tsx (remove server import, trim origins, dedupe). For src/trpc/react.tsx remove the server env import and move the logic server-side or replace with client-safe NEXT_PUBLIC_* env vars.
-import { env as serverEnv } from "@/env/server";
+// Avoid importing server env in client code
...
const TRUSTED_ORIGINS = useMemo(() => {
- const origins = env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS
- ? env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS.split(",")
- : ["https://request.network", "https://core-api-staging.pay.so"];
+ const originsBase = env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS
+ ? env.NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS.split(",")
+ : ["https://request.network", "https://core-api-staging.pay.so"];
+ const origins = originsBase.map((s) => s.trim()).filter(Boolean);
// Add localhost in development
- if (serverEnv.NODE_ENV === "development") {
- origins.push("http://localhost:3000");
- }
+ if (process.env.NODE_ENV === "development") {
+ origins.push("http://localhost:3000");
+ }
- return origins;
+ return Array.from(new Set(origins));
}, []);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { env } from "@/env/client"; | |
| import { env as serverEnv } from "@/env/server"; | |
| import { COMPLIANCE_COUNTRIES } from "@/lib/constants/compliance"; | |
| import { env } from "@/env/client"; | |
| // Avoid importing server env in client code | |
| import { COMPLIANCE_COUNTRIES } from "@/lib/constants/compliance"; |
🤖 Prompt for AI Agents
In src/components/compliance-form.tsx around lines 34 to 36, remove the import
of server-only env (import { env as serverEnv } from "@/env/server") so the
client component only uses client-safe env values; replace any usage of
serverEnv with appropriate NEXT_PUBLIC_* variables or derive values on the
server and pass them as props; also trim whitespace from the trusted origins
list and deduplicate entries (e.g., normalize and filter unique) before using
them in the client. For the other occurrence in src/trpc/react.tsx (line ~9)
remove the server env import and either move the logic that needs server env to
a server-side module or switch to NEXT_PUBLIC_* env vars so no server-only
imports remain in client code.
| import { useState } from "react"; | ||
| import superjson from "superjson"; | ||
|
|
||
| import { env } from "@/env/server"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Blocker: Server env imported in a client component (bundle break + secret leak risk).
This file is "use client". Importing @/env/server in a client bundle violates t3-env’s server-only contract and can fail the build or leak server env via experimental runtime env plumbing. Use compile-time process.env.NODE_ENV (safe in client) instead.
Apply this diff:
-import { env } from "@/env/server";
+// Avoid importing server env in client bundles.
+const isDev = process.env.NODE_ENV === "development";
@@
- env.NODE_ENV === "development" ||
+ isDev ||Run to catch any remaining server-env imports in client modules:
Also applies to: 38-38
🏁 Script executed:
#!/bin/bash
# Client files that incorrectly import server env
fd -t f -e ts -e tsx | while read -r f; do
if rg -n '^"use client";' "$f" >/dev/null; then
rg -n '@/env/server' "$f"
fi
doneLength of output: 89
Blocker: remove server-only env imports from client components (build break / secret-leak risk).
Found '@/env/server' imported inside client modules:
- src/trpc/react.tsx — line 9:
import { env } from "@/env/server"; - another client file — match at line 35:
import { env as serverEnv } from "@/env/server";
Apply this diff to the affected client files (and make analogous edits where serverEnv is used):
-import { env } from "@/env/server";
+// Avoid importing server env in client bundles.
+const isDev = process.env.NODE_ENV === "development";
@@
- env.NODE_ENV === "development" ||
+ isDev ||Re-run the verification script until no '@/env/server' imports remain in "use client" files.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { env } from "@/env/server"; | |
| // Avoid importing server env in client bundles. | |
| const isDev = process.env.NODE_ENV === "development"; |
🤖 Prompt for AI Agents
In src/trpc/react.tsx around line 9, remove the server-only import `import { env
} from "@/env/server";` from this client module; replace uses of server env
values with safe alternatives such as importing from a client-safe env module
(e.g., "@/env/client" or process.env NEXT_PUBLIC_*), or receive the needed
values via props/context from a server-safe boundary; search for other client
files that import "@/env/server" (e.g., the referenced file at line 35) and
apply the same change, ensuring no server-only imports remain in "use client"
files and re-run verification until the build passes.
| import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; | ||
| import superjson from "superjson"; | ||
|
|
||
| import { env } from "@/env/server"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Blocker: @/env/server used in an isomorphic module imported by client code.
This module is used by src/trpc/react.tsx (client). Top‑level import of server env will be bundled client‑side, causing build errors and potential secret exposure. Read non‑secret vars directly from process.env inside the function instead.
Apply this diff:
-import { env } from "@/env/server";
+// Do not import server env here; this module is used client-side too.
@@
- if (env.VERCEL_URL) return `https://${env.VERCEL_URL}`;
- return `http://localhost:${env.PORT ?? 3000}`;
+ if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
+ return `http://localhost:${process.env.PORT ?? "3000"}`;Confirm no shared/isomorphic modules import @/env/server:
Also applies to: 11-12
🏁 Script executed:
#!/bin/bash
# Any imports of server env in src/trpc or other shared code
rg -n '@/env/server' src | rg -nP '^(?:(?!/server\.ts).)*$'Length of output: 82
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo ">>> Searching for imports of '@/env/server' in src/ ..."
rg -n --hidden --no-ignore -S "@/env/server" src || true
echo
echo ">>> Showing src/trpc/shared.ts (if present) ..."
if [ -f "src/trpc/shared.ts" ]; then
sed -n '1,240p' src/trpc/shared.ts
else
echo "src/trpc/shared.ts not found"
fi
echo
echo ">>> Showing src/trpc/react.tsx (if present) ..."
if [ -f "src/trpc/react.tsx" ]; then
sed -n '1,240p' src/trpc/react.tsx
else
echo "src/trpc/react.tsx not found"
fi
echo
echo ">>> Searching for files that import src/trpc/shared (to detect client usage) ..."
rg -n --hidden --no-ignore -S "trpc/shared" src || true
rg -n --hidden --no-ignore -S "@/trpc" src || trueLength of output: 6517
Blocker: '@/env/server' imported into client-side code — fix immediately.
src/trpc/react.tsx is a "use client" module and imports "@/env/server"; src/trpc/shared.ts also imports it and is consumed by react.tsx — this will bundle server-only env (and may expose secrets / break the build). Remove server env imports from modules that flow into client bundles.
- Change src/trpc/shared.ts: stop importing "@/env/server" and read non-secret vars from process.env inside getBaseUrl. Apply this diff:
-import { env } from "@/env/server";
+// Do not import server env here; this module is used client-side too.
@@
- if (env.VERCEL_URL) return `https://${env.VERCEL_URL}`;
- return `http://localhost:${env.PORT ?? 3000}`;
+ if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
+ return `http://localhost:${process.env.PORT ?? "3000"}`;-
Change src/trpc/react.tsx: remove
import { env } from "@/env/server"and replace uses ofenv.NODE_ENVwithprocess.env.NODE_ENVor a client-safe NEXT_PUBLIC_* variable. Do not import server-only env into client modules. -
Verify other imports of "@/env/server" (e.g., src/components/compliance-form.tsx and multiple server files) are only used server-side; if any are pulled into client bundles, remove or replace with client-safe env handling.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { env } from "@/env/server"; | |
| // Do not import server env here; this module is used client-side too. | |
| export const getBaseUrl = () => { | |
| if (typeof window !== "undefined") return ""; | |
| if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; | |
| return `http://localhost:${process.env.PORT ?? "3000"}`; | |
| }; |
|
Hey @Envoy-VC , thanks a lot for your contribution to Easy Invoice, much appreciated 🙏 But your approach with separate files served as a great base for our PR, so even though we won't be merging this one, I wanted to give you a big kudos for your approach. If it's alright with you, we can close this PR. Have a virtual cookie on me 🍪 ! |
Great! closing this PR Now. |
Fixes: #110
This PR adds env validation for easy-invoice. It uses t3-env with zod for env validation before next development or production is started and throws if the zod schema throws error.
Env validation is divided into two files for clear seperation:
src/env/client.ts: Client Environment variables (NEXT_PUBLIC_)src/env/server.ts: Server Environment variables/Summary by CodeRabbit
Refactor
Chores