Skip to content

Conversation

@envoy1084
Copy link
Contributor

@envoy1084 envoy1084 commented Sep 14, 2025

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

    • Centralized and validated environment handling across the app and backend; public config (GTM ID, Terms link, demo link, payment/trusted origins) now consistently sourced.
    • Next.js config made phase-aware to support conditional env loading during build/dev.
  • Chores

    • Added environment-management dependencies and updated validation library.
    • CI build adjusted to optionally skip env validation; improved Redis error logging.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 14, 2025

Walkthrough

Converts 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 env exports.

Changes

Cohort / File(s) Summary
Next config & deps
next.config.mjs, package.json
Next config changed to export default function nextConfig(phase) and uses a JITI loader to import env modules for side-effects. Adds @t3-oss/env-nextjs, jiti and updates zod.
Env modules
src/env/server.ts, src/env/client.ts
New env exports created with createEnv (server schema validates many required vars; client schema exposes NEXT_PUBLIC_* keys). Server uses experimental__runtimeEnv = process.env.
App / Client usage
src/app/layout.tsx, src/app/page.tsx, src/components/app-kit.tsx, src/components/compliance-form.tsx, src/components/header.tsx
Replace process.env reads with env from @/env/client for GTM ID, Terms URL, project ID, trusted origins, and demo meeting URL.
API routes / middleware / trpc
src/app/api/webhook/route.ts, src/app/api/trpc/[trpc]/route.ts, src/app/login/google/route.ts, src/middleware.ts, src/trpc/react.tsx, src/trpc/server.ts, src/trpc/shared.ts
Replace process.env with env from @/env/server for WEBHOOK_SECRET, NODE_ENV, VERCEL_URL, PORT and related checks; preserves existing behavior.
Server libs
src/lib/axios.ts, src/lib/encryption/index.ts, src/lib/redis/index.ts, src/server/db/index.ts, src/server/db/schema.ts
Switch configuration sources to env (REQUEST_API_*, ENCRYPTION_KEY, REDIS_URL, DATABASE_URL, etc.). Adds Redis error listener and uses env-sourced defaults/fallbacks.
Routers
src/server/routers/invoice.ts, src/server/routers/payment.ts
Fee percentage/address and TTL now read from env instead of process.env; behavior and payloads unchanged.
CI workflow
.github/workflows/build-and-lint.yml
Sets SKIP_ENV_VALIDATION: "1" during build step to skip env validation in CI.

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
Loading
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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • rodrigopavezi
  • aimensahnoun
  • MantisClone

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: env validation" accurately describes the main change in this pull request, which introduces comprehensive environment variable validation using the @t3-oss/env-nextjs library with Zod schemas. The changeset systematically replaces direct process.env access throughout the codebase with validated environment variables from centralized env modules, implements fail-fast validation on startup, and adds the necessary configuration in next.config.mjs to load these validation modules during development and production builds. The title is concise, follows conventional commit format, and clearly communicates the primary purpose of adding environment validation to the application.
Linked Issues Check ✅ Passed The pull request fully addresses the requirements from linked issue #110, which asked for testing all required environment variables on application startup and failing fast when required vars are missing or invalid. The implementation adds comprehensive environment validation using @t3-oss/env-nextjs with Zod schemas that validate both client-side (NEXT_PUBLIC_*) and server-side environment variables, with validation occurring during Next.js development server startup and production builds through the updated next.config.mjs configuration. The solution replaces all direct process.env access throughout the codebase with validated environment variables, ensuring that misconfigured or missing required variables (like the Webhook Secret mentioned in the issue) will cause the application to fail fast on startup rather than at runtime.
Out of Scope Changes Check ✅ Passed All changes in this pull request are directly related to implementing environment variable validation as specified in issue #110. The systematic replacement of process.env access with validated env imports across all application files, the addition of client and server environment validation modules, the Next.js configuration updates to load these modules at startup, the package.json updates to include necessary dependencies (@t3-oss/env-nextjs, jiti, updated zod), and the CI workflow modification to skip validation during builds are all necessary components for implementing comprehensive environment validation. No unrelated or out-of-scope changes were identified in the changeset.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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: x
src/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

📥 Commits

Reviewing files that changed from the base of the PR and between e18bbf9 and ab0ac27.

⛔ Files ignored due to path filters (1)
  • package-lock.json is 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 verified

tsconfig.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 next or 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",
Copy link
Contributor

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.

Comment on lines 1 to 19
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,
},
});
Copy link
Contributor

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
    done

Length 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
    done

Length 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(),
Copy link
Contributor

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.

Suggested change
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.

Comment on lines 4 to 21
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,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

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.

Suggested change
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).

Comment on lines +5 to 9
baseURL: env.REQUEST_API_URL,
headers: {
"x-api-key": env.REQUEST_API_KEY,
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +378 to 383
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);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines 451 to 455
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +68 to 70
feePercentage: env.FEE_PERCENTAGE_FOR_PAYMENT,
feeAddress: env.FEE_ADDRESS_FOR_PAYMENT,
}))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between ab0ac27 and 2429d97.

📒 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 failed

Verification 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.

Comment on lines +94 to 96
env:
SKIP_ENV_VALIDATION: "1"
run: npm run build
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +34 to 36
import { env } from "@/env/client";
import { env as serverEnv } from "@/env/server";
import { COMPLIANCE_COUNTRIES } from "@/lib/constants/compliance";
Copy link
Contributor

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
done

Length 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"
done

Length 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.

Suggested change
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";
Copy link
Contributor

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
done

Length 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.

Suggested change
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";
Copy link
Contributor

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 || true

Length 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 of env.NODE_ENV with process.env.NODE_ENV or 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.

Suggested change
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"}`;
};

@bassgeta
Copy link
Contributor

Hey @Envoy-VC , thanks a lot for your contribution to Easy Invoice, much appreciated 🙏
After looking through it, we decided to implement something simpler here.
We ended up just validating the environment without re-exporting the variables.

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 🍪 !

@envoy1084
Copy link
Contributor Author

Hey @Envoy-VC , thanks a lot for your contribution to Easy Invoice, much appreciated 🙏 After looking through it, we decided to implement something simpler here. We ended up just validating the environment without re-exporting the variables.

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.

@envoy1084 envoy1084 closed this Sep 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EasyInvoice - Test env vars on startup. Fail fast on misconfigured env vars

2 participants