Skip to content

Runtime environment failfast#666

Merged
kentcdodds merged 15 commits intomainfrom
cursor/runtime-environment-failfast-75bb
Feb 23, 2026
Merged

Runtime environment failfast#666
kentcdodds merged 15 commits intomainfrom
cursor/runtime-environment-failfast-75bb

Conversation

@kentcdodds
Copy link
Owner

@kentcdodds kentcdodds commented Feb 23, 2026

Make runtime environment configuration fail fast by removing all fallbacks and requiring explicit configuration for production.


Open in Web Open in Cursor 


Note

Medium Risk
Touches cross-cutting config used by startup, auth/session cookies, external integrations, and production routing; misconfigured env or subtle behavior changes could cause runtime failures despite the changes being largely mechanical.

Overview
Centralizes environment configuration and makes it fail-fast. A new Zod-backed getEnv() (cached) validates required runtime env vars, derives values like DATABASE_PATH/allowedActionOrigins, and introduces a separate getPublicEnv() to limit what is exposed to the client.

Updates many server/app modules (Discord, email, Prisma, Sentry tunnel, semantic search, OAuth, cache tooling, etc.) to read config exclusively via getEnv() instead of process.env/getRequiredServerEnvVar, removing development fallbacks and tightening production behavior (including safer Sentry tunnel validation).

Adjusts ops/tooling: .env.example adds required keys like PORT/FLY_APP_NAME/DATABASE_PATH and simplifies YouTube playlist config to YOUTUBE_PLAYLIST_ID; the YouTube indexing workflow/script/docs follow suit; Playwright/tests/Prisma CLI are updated to ensure env is present without weakening runtime fail-fast guarantees.

Written by Cursor Bugbot for commit 171654a. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Added DATABASE_PATH, PORT, and FLY_APP_NAME; introduced a safe public ENV surface for client-side use.
  • Bug Fixes

    • Hardened Sentry envelope parsing/validation and improved error responses for malformed or misrouted payloads.
  • Refactor

    • Centralized environment access and validation with derived defaults and runtime checks across the app.
  • Tests

    • Test setup now loads .env and .env.example as non-overriding fallbacks; Playwright ensures NODE_ENV is set.

@cursor
Copy link

cursor bot commented Feb 23, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Centralizes environment access by introducing a layered env schema, getEnv() and getPublicEnv(), and replaces direct process.env and legacy helpers across the app; updates runtime consumers, bootstrap, Prisma config, tests, and many service integrations to use the new env abstraction.

Changes

Cohort / File(s) Summary
Env schema & example
\.env.example, app/utils/env.server.ts
Added layered Zod schema, many new vars (PORT, DATABASE_PATH, FLY_APP_NAME, LITEFS_DIR, etc.), validation, derived helpers, memoization, and new accessors getEnv() and getPublicEnv().
App entry, root & error UI
app/entry.server.tsx, app/root.tsx, app/components/errors.tsx
Initialize global.ENV with getPublicEnv(); switch dev checks to import.meta.env.MODE/public ENV and replace process.env usages with env-derived values.
Server bootstrap & runtime
server/index.ts, server/startup-shortcuts.ts, server/utils/monitoring.ts, server/rate-limiting.ts, server/expired-sessions-cleanup.ts
Switched startup/runtime to use getEnv()/getPublicEnv() (MODE, PORT, FLY_*, SENTRY, allowedActionOrigins); compute expired-sessions enablement from env; adjusted rate-limiting and monitoring to read env via accessor.
Prisma & tests
prisma.config.ts, tests/setup-env.ts, playwright.config.ts
Prisma datasource URL computed from DATABASE_URL/DATABASE_PATH/filename fallback; tests load .env and .env.example; ensure NODE_ENV present for Playwright.
Routes & resource handlers
app/routes/...
app/routes/action/refresh-cache.tsx, app/routes/resources/cache.sqlite.ts, app/routes/resources/lookout.ts, app/routes/resources/calls/save.tsx, app/routes/resources/calls/text-to-speech.tsx, app/routes/calls_.record/new.tsx, app/routes/oauth.authorize.tsx, app/routes/youtube.tsx
Replaced token/secret lookups with getEnv(); lookout route hardened with envelope parsing/validation using env-derived Sentry host/project IDs; several routes now read env via accessor.
Auth, sessions & misc helpers
app/utils/session.server.ts, app/utils/login.server.ts, app/utils/encryption.server.ts, app/utils/misc.ts, app/utils/client.server.ts, app/utils/webauthn.server.ts
Removed legacy getRequired* helpers, replaced with getEnv() reads (SESSION_SECRET, MAGIC_LINK_SECRET, etc.), and removed old helper exports; misc functions read ENV directly when appropriate.
Third‑party integrations & service configs
app/kit/kit.server.ts, app/utils/discord.server.ts, app/utils/simplecast.server.ts, app/utils/transistor.server.ts, app/utils/github.server.ts, app/utils/verifier.server.ts
Centralized per-call config getters (getKitAuth, getDiscordConfig, getSimplecastConfig); credentials and IDs now read from getEnv() rather than module-level constants or process.env.
Cloudflare AI, TTS/ASR, semantic search
app/utils/cloudflare-ai-text-to-speech.server.ts, app/utils/cloudflare-ai-transcription.server.ts, app/routes/resources/calls/text-to-speech.tsx, app/utils/semantic-search.server.ts, app/utils/semantic-search-admin.server.ts
Switched model/account/token reads to getEnv(); updated defaulting and runtime guards; some service return objects now include getEnv where needed.
Cache, lookup, and metadata utilities
app/utils/cache.server.ts, app/utils/blog.server.ts, app/utils/x.server.ts, app/utils/prisma.server.ts, app/utils/semantic-search-admin.server.ts
Replaced hardcoded env constants with dynamic getEnv() values (cache DB path, leaderboard channel, mocks flag, DB URL), and adapted filesystem/error messages to use derived paths.
Monitoring & Sentry-related handling
app/routes/resources/lookout.ts, server/utils/monitoring.ts, server/index.ts
Sentry config now derived from env (SENTRY_DSN, SENTRY_PROJECT_ID); lookout parsing validates DSN/host/project and returns appropriate 400/404/502 errors on malformed/unsupported envelopes.
Small helpers & tests
tests/setup-env.ts, playwright.config.ts
Test env loading via dotenv (.env + .env.example); ensure NODE_ENV set for Playwright runs.
Dotfile example
\.env.example
Added DATABASE_PATH and PORT defaults; updated auth fallback comment and added FLY_APP_NAME example.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server
    participant EnvModule as env.server.ts
    participant Database as Prisma
    participant Sentry
    participant External as ExternalAPIs

    Client->>Server: HTTP request / startup
    Server->>EnvModule: call getPublicEnv() / getEnv()
    EnvModule-->>Server: validated env (MODE, PORT, SENTRY_DSN, DATABASE_URL, ...)
    Server->>Database: connect using DATABASE_URL / DATABASE_PATH
    Server->>Sentry: init if SENTRY_DSN present
    Server->>External: call external services (Discord, Cloudflare, Transistor...) using credentials from EnvModule
    External-->>Server: responses
    Server-->>Client: HTTP response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through the lines with careful delight,

Gathered env vars neat in burrows at night.
getEnv keeps secrets warm, public bits small,
No more wild process.env chaos in the hall.
A tidy warren now — carrots for all!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Runtime environment failfast' directly describes the main objective of this PR - centralizing environment configuration to fail fast when required variables are missing, rather than using fallbacks.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cursor/runtime-environment-failfast-75bb

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.

@kentcdodds kentcdodds marked this pull request as ready for review February 23, 2026 05:25
Copy link

@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: 6

🧹 Nitpick comments (7)
app/kit/kit.server.ts (1)

113-120: getKitAuth() is called redundantly within the same call chain.

In both addTagToSubscriber and tagKCDSiteSubscriber, getKitSubscriber (called just above) already invokes getKitAuth() internally. The subsequent direct call on lines 114 and 148 is redundant. getEnv()'s fingerprint cache makes this harmless, but it can be simplified by having the inner functions accept credentials as parameters, or by only calling getKitAuth() once at the top of each function and passing the result down.

Also applies to: 145-151

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/kit/kit.server.ts` around lines 113 - 120, The code redundantly calls
getKitAuth() after calling getKitSubscriber(), which already obtains
credentials; fix by calling getKitAuth() only once and passing those credentials
into downstream calls or by changing getKitSubscriber/getKitSubscriber's callers
to return/accept auth. Specifically, update addTagToSubscriber and
tagKCDSiteSubscriber to call getKitAuth() at the top (or accept an auth param),
remove the second getKitAuth() calls on lines where subscriberData is built, and
refactor getKitSubscriber (or its usages) to accept apiKey/apiSecret or to
return { subscriber, apiKey, apiSecret } so you reuse the same credentials
instead of re-invoking getKitAuth().
tests/setup-env.ts (1)

11-16: Optional: consolidate into a single loadDotenv call with path array.

Dotenv accepts an array of paths and processes them in order; the first value set for a variable wins when override is false. Since quiet: true already silences missing-file warnings, the existsSync guard is redundant.

♻️ Proposed simplification
-const cwd = process.cwd()
-const dotenvPath = path.join(cwd, '.env')
-if (fs.existsSync(dotenvPath)) {
-	loadDotenv({ path: dotenvPath, override: false, quiet: true })
-}
-loadDotenv({ path: path.join(cwd, '.env.example'), override: false, quiet: true })
+loadDotenv({
+	path: [path.join(process.cwd(), '.env'), path.join(process.cwd(), '.env.example')],
+	override: false,
+	quiet: true,
+})

If you prefer to keep the guard for explicitness, that's fine too — the existing code is correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/setup-env.ts` around lines 11 - 16, The code redundantly checks
fs.existsSync(dotenvPath) before calling loadDotenv; consolidate by calling
loadDotenv once with an ordered array of paths ([path.join(cwd, '.env'),
path.join(cwd, '.env.example')]) so variables from .env take precedence and
override: false plus quiet: true maintain behavior—remove the fs.existsSync
guard and replace the two loadDotenv calls with a single loadDotenv({ path: [
... ], override: false, quiet: true }) call referencing cwd, dotenvPath, and
loadDotenv.
app/utils/github.server.ts (1)

7-14: Module-level getEnv() freezes ref and the auth token at import time.

getEnv() is called at module scope (lines 7 and 14), which means:

  1. Eager validation — importing this module triggers the full Zod schema validation. If github.server.ts is transitively pulled in before init() executes (e.g. via blog.server.tsgithub.server.ts during server startup), the resulting ZodError will surface without the formatted init() message.
  2. Static singletonref and the octokit auth token are captured once and cannot be updated without a server restart; dynamic env-var reload (e.g. secret rotation) won't take effect.

Both consequences are acceptable given the PR's fail-fast goals and the project's restart-to-rotate deployment model, but this is worth keeping in mind if secret rotation without restarts is ever desired. If lazy evaluation is preferred, moving both reads inside downloadFile/downloadDirList (and creating octokit lazily) would avoid the eager-import side-effect at the cost of slightly more boilerplate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/github.server.ts` around lines 7 - 14, The module currently calls
getEnv() at top-level (producing the ref constant and constructing octokit)
which freezes env validation and auth at import time; change this by removing
module-level getEnv() usage and instead read env lazily inside the functions
that need them (e.g., downloadFile and downloadDirList): create a small helper
like getRef() that calls getEnv().GITHUB_REF on demand and create a getOctokit()
factory that constructs and returns a new Octokit instance using
getEnv().BOT_GITHUB_TOKEN when first needed (or each call if you prefer fresh
creds); update downloadFile/downloadDirList to call getRef() and getOctokit()
rather than using the module-level ref/octokit so env validation and tokens are
resolved at call time.
app/utils/webauthn.server.ts (1)

7-14: Module-level getEnv() calls enforce fail-fast on import.

Unlike simplecast.server.ts which defers env reads to function call time via getSimplecastConfig(), this file evaluates getEnv() at module load time (lines 12–13). This means any module that imports webauthn.server.ts will fail immediately if env vars aren't configured — which is the PR's intent, but may cause issues in test environments where this file is transitively imported before env setup.

Consider consolidating the two getEnv() calls if you'd like a minor cleanup:

Optional: single destructure
+const { NODE_ENV, SESSION_SECRET } = getEnv()
+
 export const passkeyCookie = createCookie('webauthn-challenge', {
 	path: '/',
 	sameSite: 'lax',
 	httpOnly: true,
 	maxAge: 60 * 60 * 2,
-	secure: getEnv().NODE_ENV === 'production',
-	secrets: [getEnv().SESSION_SECRET],
+	secure: NODE_ENV === 'production',
+	secrets: [SESSION_SECRET],
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/webauthn.server.ts` around lines 7 - 14, The module calls getEnv()
twice at import time when constructing passkeyCookie (secure and secrets),
causing fail-fast imports; change to call getEnv() once inside this module
(e.g., const { NODE_ENV, SESSION_SECRET } = getEnv()) and use those variables
when creating passkeyCookie so the env read is consolidated and clear, or
alternatively defer the getEnv() call into a function that returns the cookie if
you need to avoid import-time reads; update references in the passkeyCookie
creation to use the new local variables or factory function and keep the
createCookie invocation otherwise unchanged.
server/index.ts (1)

85-85: Sentry context label 'region' is set to FLY_INSTANCE (the instance ID), not FLY_REGION.

This is likely pre-existing, but the Sentry context name 'region' is misleading when the value is the instance identifier. Consider using env.FLY_REGION here, or renaming the context to 'instance'.

Suggested fix
-	sentrySetContext('region', { name: env.FLY_INSTANCE })
+	sentrySetContext('region', { name: env.FLY_REGION })

Or, if instance-level granularity is intentional:

-	sentrySetContext('region', { name: env.FLY_INSTANCE })
+	sentrySetContext('instance', { name: env.FLY_INSTANCE })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/index.ts` at line 85, The Sentry context call uses
sentrySetContext('region', { name: env.FLY_INSTANCE }) but supplies an instance
ID, not a region; update the call to either use the region env variable or
rename the context to reflect instance-level data: replace with
sentrySetContext('region', { name: env.FLY_REGION }) if you want region
granularity, or change to sentrySetContext('instance', { name: env.FLY_INSTANCE
}) if instance-level info is intended; locate the call to sentrySetContext in
server/index.ts to make this change.
app/utils/cloudflare-ai-transcription.server.ts (1)

17-37: Double getEnv() call in transcribeMp3WithWorkersAi.

getEnv() is invoked once for the default parameter (Line 19) and again inside the function body (Line 30). Since getEnv() is fingerprint-cached this has negligible cost, but you could simplify by reading the model inside the body instead of as a default parameter:

♻️ Optional: single getEnv() call
 export async function transcribeMp3WithWorkersAi({
 	mp3,
-	model = getEnv().CLOUDFLARE_AI_TRANSCRIPTION_MODEL,
+	model,
 }: {
 	mp3: Uint8Array
 	model?: string
 }): Promise<string> {
 	const env = getEnv()
+	const resolvedModel = model ?? env.CLOUDFLARE_AI_TRANSCRIPTION_MODEL
 	const accountId = env.CLOUDFLARE_ACCOUNT_ID
 	const apiToken = env.CLOUDFLARE_API_TOKEN

Then use resolvedModel in the rest of the function.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/cloudflare-ai-transcription.server.ts` around lines 17 - 37, The
function transcribeMp3WithWorkersAi calls getEnv() twice (once in the default
parameter and again in the body); simplify by removing getEnv() from the
parameter default, call const env = getEnv() once inside the function, then
derive the model with something like const resolvedModel = model ??
env.CLOUDFLARE_AI_TRANSCRIPTION_MODEL and use resolvedModel for the rest of the
logic (keep the parameter signature as model?: string and update usages to
resolvedModel).
app/utils/transistor.server.ts (1)

374-374: Cache key includes dynamic podcast ID — verify stability.

The cache key transistor:episodes:${getEnv().CALL_KENT_PODCAST_ID} is evaluated each time getCachedEpisodes is called. If CALL_KENT_PODCAST_ID ever changes between calls (e.g., env hot-reload), the old cache entry becomes orphaned. This is unlikely in practice but worth noting — the previous static constant would never have had this issue.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/transistor.server.ts` at line 374, The cache key episodesCacheKey
is being rebuilt inside getCachedEpisodes using getEnv().CALL_KENT_PODCAST_ID
which can change across calls and orphan previous cache entries; fix this by
computing the key once (or accepting the podcast id as an explicit parameter)
and reusing it—e.g., create a module-level constant like
TRANSISTOR_EPISODES_CACHE_KEY that captures getEnv().CALL_KENT_PODCAST_ID at
import time, or adjust getCachedEpisodes to accept podcastId and build the key
from that single stable value so episodesCacheKey remains stable across calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Line 37: The new .env.example entry DATABASE_PATH="./prisma/sqlite.db"
triggers dotenv-linter's QuoteCharacter rule; update the DATABASE_PATH entry to
remove the surrounding double quotes (make it DATABASE_PATH=./prisma/sqlite.db)
so it matches the unquoted convention used for PORT and satisfies the linter;
ensure no other entries are accidentally changed and run dotenv-linter to verify
the warning is resolved.
- Line 45: Remove the unnecessary quotes around the FLY_APP_NAME value to
satisfy dotenv-linter's QuoteCharacter rule: update the .env example entry for
the FLY_APP_NAME variable (currently FLY_APP_NAME="kcd") to an unquoted form
(FLY_APP_NAME=kcd) so it matches other unquoted entries like PORT and
DATABASE_PATH and eliminates the linter warning.

In `@app/kit/kit.server.ts`:
- Around line 24-26: The code currently appends apiSecret from getKitAuth() into
the URL query string (the url variable) which can leak secrets in logs; instead,
stop setting api_secret on URL.searchParams and send the credential in an
Authorization header (e.g., Authorization: Bearer <apiSecret>) or in the POST
body when calling Kit; update the fetch calls in getKitSubscriber and
getKitSubscriberTags to remove query-based api_secret, set the header using the
apiSecret returned by getKitAuth(), and if the endpoint requires GET semantics,
switch to a POST with an equivalent request body so secrets are never placed in
the URL.

In `@app/utils/env.server.ts`:
- Around line 211-226: The init function's catch block only preserves ZodError
details and rethrows a generic Error, losing original error context for non-Zod
failures from getEnv(); update init to preserve and surface the original error:
when catching, if error is a z.ZodError keep the existing detailed console.error
output, and for any other unknown error log its message/stack (or include it in
the thrown Error) and rethrow the original error (or throw a new Error that
includes the original error.message/stack) so callers see the real cause;
reference init, getEnv, z.ZodError and parsedEnv when making this change.
- Around line 191-196: The environment schema validation
(schema.safeParse(process.env) and the parsed.success check that throws
parsed.error) fails locally because NODE_ENV is not set for the local Playwright
dev flow; update the test:e2e:dev setup to export NODE_ENV=development (e.g.,
add NODE_ENV=development to the cross-env invocation used to launch the dev
server) or alternatively add NODE_ENV to the project .env and .env.example and
document that developers must populate it before running npm run test:e2e:dev so
schema.safeParse receives a valid NODE_ENV value.
- Around line 181-189: In getEnv, remove the unnecessary double cast around
schemaBase when accessing shape: replace the Object.keys((schemaBase as unknown
as any).shape) access with a direct access to schemaBase.shape and keep the keys
typed as Array<keyof BaseEnv>; update the line that defines keys in the getEnv
function to use schemaBase.shape without the as unknown as any cast so
TypeScript uses the ZodObject typing correctly.

---

Nitpick comments:
In `@app/kit/kit.server.ts`:
- Around line 113-120: The code redundantly calls getKitAuth() after calling
getKitSubscriber(), which already obtains credentials; fix by calling
getKitAuth() only once and passing those credentials into downstream calls or by
changing getKitSubscriber/getKitSubscriber's callers to return/accept auth.
Specifically, update addTagToSubscriber and tagKCDSiteSubscriber to call
getKitAuth() at the top (or accept an auth param), remove the second
getKitAuth() calls on lines where subscriberData is built, and refactor
getKitSubscriber (or its usages) to accept apiKey/apiSecret or to return {
subscriber, apiKey, apiSecret } so you reuse the same credentials instead of
re-invoking getKitAuth().

In `@app/utils/cloudflare-ai-transcription.server.ts`:
- Around line 17-37: The function transcribeMp3WithWorkersAi calls getEnv()
twice (once in the default parameter and again in the body); simplify by
removing getEnv() from the parameter default, call const env = getEnv() once
inside the function, then derive the model with something like const
resolvedModel = model ?? env.CLOUDFLARE_AI_TRANSCRIPTION_MODEL and use
resolvedModel for the rest of the logic (keep the parameter signature as model?:
string and update usages to resolvedModel).

In `@app/utils/github.server.ts`:
- Around line 7-14: The module currently calls getEnv() at top-level (producing
the ref constant and constructing octokit) which freezes env validation and auth
at import time; change this by removing module-level getEnv() usage and instead
read env lazily inside the functions that need them (e.g., downloadFile and
downloadDirList): create a small helper like getRef() that calls
getEnv().GITHUB_REF on demand and create a getOctokit() factory that constructs
and returns a new Octokit instance using getEnv().BOT_GITHUB_TOKEN when first
needed (or each call if you prefer fresh creds); update
downloadFile/downloadDirList to call getRef() and getOctokit() rather than using
the module-level ref/octokit so env validation and tokens are resolved at call
time.

In `@app/utils/transistor.server.ts`:
- Line 374: The cache key episodesCacheKey is being rebuilt inside
getCachedEpisodes using getEnv().CALL_KENT_PODCAST_ID which can change across
calls and orphan previous cache entries; fix this by computing the key once (or
accepting the podcast id as an explicit parameter) and reusing it—e.g., create a
module-level constant like TRANSISTOR_EPISODES_CACHE_KEY that captures
getEnv().CALL_KENT_PODCAST_ID at import time, or adjust getCachedEpisodes to
accept podcastId and build the key from that single stable value so
episodesCacheKey remains stable across calls.

In `@app/utils/webauthn.server.ts`:
- Around line 7-14: The module calls getEnv() twice at import time when
constructing passkeyCookie (secure and secrets), causing fail-fast imports;
change to call getEnv() once inside this module (e.g., const { NODE_ENV,
SESSION_SECRET } = getEnv()) and use those variables when creating passkeyCookie
so the env read is consolidated and clear, or alternatively defer the getEnv()
call into a function that returns the cookie if you need to avoid import-time
reads; update references in the passkeyCookie creation to use the new local
variables or factory function and keep the createCookie invocation otherwise
unchanged.

In `@server/index.ts`:
- Line 85: The Sentry context call uses sentrySetContext('region', { name:
env.FLY_INSTANCE }) but supplies an instance ID, not a region; update the call
to either use the region env variable or rename the context to reflect
instance-level data: replace with sentrySetContext('region', { name:
env.FLY_REGION }) if you want region granularity, or change to
sentrySetContext('instance', { name: env.FLY_INSTANCE }) if instance-level info
is intended; locate the call to sentrySetContext in server/index.ts to make this
change.

In `@tests/setup-env.ts`:
- Around line 11-16: The code redundantly checks fs.existsSync(dotenvPath)
before calling loadDotenv; consolidate by calling loadDotenv once with an
ordered array of paths ([path.join(cwd, '.env'), path.join(cwd,
'.env.example')]) so variables from .env take precedence and override: false
plus quiet: true maintain behavior—remove the fs.existsSync guard and replace
the two loadDotenv calls with a single loadDotenv({ path: [ ... ], override:
false, quiet: true }) call referencing cwd, dotenvPath, and loadDotenv.

Comment on lines +24 to +26
const { apiSecret } = getKitAuth()
const url = new URL('https://api.kit.com/v3/subscribers')
url.searchParams.set('api_secret', KIT_API_SECRET)
url.searchParams.set('api_secret', apiSecret)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

api_secret in URL query string leaks to server logs.

These are server-to-server HTTPS calls so in-transit exposure is not the concern, but servers will often log/save query parameters for requests for a long time, meaning the secret will appear in cleartext in your infra's outbound HTTP access logs and in Kit's inbound logs. The Kit v3 API also accepts credentials via the request body or an Authorization header; moving to either would avoid log exposure.

🔒 Alternative: pass `api_secret` in the request body (for the GET-equivalent, use POST or an Authorization header)

For getKitSubscriber and getKitSubscriberTags, Kit v3 supports api_secret in the POST body as well. Alternatively, use the Authorization: Bearer <api_secret> header pattern if the API supports it, keeping secrets out of logged URLs.

-	url.searchParams.set('api_secret', apiSecret)
+	// pass credentials in request body or Authorization header instead

Also applies to: 39-41

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/kit/kit.server.ts` around lines 24 - 26, The code currently appends
apiSecret from getKitAuth() into the URL query string (the url variable) which
can leak secrets in logs; instead, stop setting api_secret on URL.searchParams
and send the credential in an Authorization header (e.g., Authorization: Bearer
<apiSecret>) or in the POST body when calling Kit; update the fetch calls in
getKitSubscriber and getKitSubscriberTags to remove query-based api_secret, set
the header using the apiSecret returned by getKitAuth(), and if the endpoint
requires GET semantics, switch to a POST with an equivalent request body so
secrets are never placed in the URL.

@cursor

This comment has been minimized.

Copy link

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

🧹 Nitpick comments (1)
app/components/errors.tsx (1)

87-89: LGTM — idiomatic Vite mode check.

Switching from process.env.NODE_ENV to import.meta.env.MODE is the correct approach for client/isomorphic components in a Vite project, and is safe for SSR-rendered error boundaries since Vite injects import.meta.env into the SSR bundle as well.

Optionally, import.meta.env.DEV (the purpose-built Vite boolean) is a slightly more concise alternative:

✨ Optional simplification using import.meta.env.DEV
-{error && import.meta.env.MODE === 'development' ? (
+{error && import.meta.env.DEV ? (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/errors.tsx` around lines 87 - 89, Replace the Vite mode string
comparison with the DEV boolean for conciseness: in the JSX conditional that
renders <RedBox error={error} /> when error exists, change the runtime check
from import.meta.env.MODE === 'development' to import.meta.env.DEV so the RedBox
render uses the built-in Vite boolean; keep the rest of the conditional and the
RedBox prop unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/components/errors.tsx`:
- Around line 87-89: Replace the Vite mode string comparison with the DEV
boolean for conciseness: in the JSX conditional that renders <RedBox
error={error} /> when error exists, change the runtime check from
import.meta.env.MODE === 'development' to import.meta.env.DEV so the RedBox
render uses the built-in Vite boolean; keep the rest of the conditional and the
RedBox prop unchanged.

Copy link

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/utils/env.server.ts`:
- Around line 184-187: The code in getEnv unnecessarily casts schemaBase to
unknown and any to access .shape; remove the double cast and access
schemaBase.shape directly (e.g., use Object.keys(schemaBase.shape) typed as
Array<keyof BaseEnv>) so the Zod v4 ZodObject typing for .shape is used and the
redundant (schemaBase as unknown as any) cast is eliminated.
- Around line 218-226: The catch block inside getEnv currently swallows
non-ZodError exceptions and rethrows a generic Error; update the catch to
preserve and surface the original error by either rethrowing the original error
(throw error) for non-z.ZodError cases or include the original error as the
cause when creating the new Error (e.g., new Error('Invalid environment
variables', { cause: error })) so non-zod runtime errors are not lost; keep the
existing z.ZodError handling that logs flattened fieldErrors but ensure all
other branches propagate the original error information.

Copy link

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

🧹 Nitpick comments (2)
app/utils/env.server.ts (2)

184-188: Redundant String(k) cast.

k is already typed as keyof BaseEnv which extends string, so the String(k) conversion on line 187 is a no-op.

♻️ Proposed simplification
-		.map((k) => `${String(k)}=${process.env[String(k)] ?? ''}`)
+		.map((k) => `${k}=${process.env[k] ?? ''}`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/env.server.ts` around lines 184 - 188, The String(k) casts in
getEnv are redundant: in the function getEnv(), keys is typed as Array<keyof
BaseEnv> where keyof BaseEnv extends string, so replace occurrences of String(k)
with k (e.g., in the .map callback and the process.env lookup) to simplify the
fingerprint computation that references schemaBase.shape, keys, and fingerprint;
ensure TypeScript still compiles since process.env accepts string keys.

212-229: void parsedEnv is addressable — previous non-ZodError logging comment has been resolved.

The existing previous review comment about swallowing non-ZodError exceptions has been addressed (lines 222-224 now log the unexpected error). ✅

One optional simplification: since init() is purely a validation side-effect (never returns parsedEnv to callers), the let parsedEnv + void parsedEnv dance can be replaced with a direct call:

♻️ Proposed simplification
 export function init() {
-	let parsedEnv: Env
 	try {
-		parsedEnv = getEnv()
+		getEnv()
 	} catch (error: unknown) {
 		if (error instanceof z.ZodError) {
 			console.error(
 				'❌ Invalid environment variables:',
 				z.flattenError(error).fieldErrors,
 			)
 		} else {
 			console.error('❌ Unexpected error while validating environment:', error)
 		}
 		throw new Error('Invalid environment variables')
 	}
-	// Keep unused warning quiet (and make debugging easier if needed).
-	void parsedEnv
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils/env.server.ts` around lines 212 - 229, The init() function performs
validation as a side-effect but unnecessarily declares parsedEnv and then voids
it; simplify by removing the parsedEnv variable and instead call getEnv()
directly inside the try block (retain existing catch behavior), e.g. replace
"let parsedEnv; parsedEnv = getEnv(); ... void parsedEnv" with a single call to
getEnv() so the function still throws on validation errors but no longer keeps
an unused binding; update references to parsedEnv accordingly and remove the
now-unused parsedEnv declaration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/utils/env.server.ts`:
- Around line 184-188: The String(k) casts in getEnv are redundant: in the
function getEnv(), keys is typed as Array<keyof BaseEnv> where keyof BaseEnv
extends string, so replace occurrences of String(k) with k (e.g., in the .map
callback and the process.env lookup) to simplify the fingerprint computation
that references schemaBase.shape, keys, and fingerprint; ensure TypeScript still
compiles since process.env accepts string keys.
- Around line 212-229: The init() function performs validation as a side-effect
but unnecessarily declares parsedEnv and then voids it; simplify by removing the
parsedEnv variable and instead call getEnv() directly inside the try block
(retain existing catch behavior), e.g. replace "let parsedEnv; parsedEnv =
getEnv(); ... void parsedEnv" with a single call to getEnv() so the function
still throws on validation errors but no longer keeps an unused binding; update
references to parsedEnv accordingly and remove the now-unused parsedEnv
declaration.

Copy link

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

🧹 Nitpick comments (1)
.env.example (1)

37-45: No startup risk, but .env.example should include MOCKS=true for dev clarity.

MOCKS is correctly marked as .optional() in the schema, so fresh clones won't fail. However, when not set, it defaults to false rather than true. Since MOCKS=true is the expected default for local development, add MOCKS=true to .env.example to document this behavior and avoid surprising developers who might not expect mocks to be disabled by default in dev.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 37 - 45, The .env.example is missing the MOCKS
setting which should document the expected local-dev default; add a line
MOCKS=true to the example so developers see that mocks are enabled by default
locally (the schema marks MOCKS as .optional() but the example should show
MOCKS=true to avoid confusion). Ensure the new variable name is exactly MOCKS
and formatted like the other entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.env.example:
- Around line 37-45: The .env.example is missing the MOCKS setting which should
document the expected local-dev default; add a line MOCKS=true to the example so
developers see that mocks are enabled by default locally (the schema marks MOCKS
as .optional() but the example should show MOCKS=true to avoid confusion).
Ensure the new variable name is exactly MOCKS and formatted like the other
entries.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 949dc22 and f4d9f6d.

📒 Files selected for processing (2)
  • .env.example
  • app/kit/kit.server.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/kit/kit.server.ts

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON. A Cloud Agent has been kicked off to fix the reported issue.

@cursor
Copy link

cursor bot commented Feb 23, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: NODE_ENV default weakens fail-fast for production deployments
    • Removed the optional default so NODE_ENV is required and missing values fail validation at startup.

cursoragent and others added 15 commits February 23, 2026 10:47
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
…ture. Emphasize concise project-specific guidance and link to detailed agent documentation in `docs/agents/`.
…te environment variables and documentation accordingly.
@kentcdodds kentcdodds force-pushed the cursor/runtime-environment-failfast-75bb branch from 3ca7b42 to 171654a Compare February 23, 2026 17:51
@kentcdodds kentcdodds merged commit 302f753 into main Feb 23, 2026
1 of 2 checks passed
@kentcdodds kentcdodds deleted the cursor/runtime-environment-failfast-75bb branch February 23, 2026 18:06
This was referenced Feb 23, 2026
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.

2 participants