Files
findyourpilot/.agent/skills/tanstack-start-best-practices/rules/env-functions.md
2026-03-02 21:16:26 +01:00

212 lines
5.3 KiB
Markdown

# env-functions: Use Environment Functions for Configuration
## Priority: MEDIUM
## Explanation
Environment functions provide type-safe access to environment variables on the server. They ensure secrets stay server-side, provide validation, and enable different configurations per environment (development, staging, production).
## Bad Example
```tsx
// Accessing env vars directly - no validation, potential leaks
export const getApiData = createServerFn()
.handler(async () => {
// No validation - may be undefined
const apiKey = process.env.API_KEY
// Accidentally exposed in error messages
if (!apiKey) {
throw new Error(`Missing API_KEY: ${process.env}`)
}
return fetch(url, { headers: { Authorization: apiKey } })
})
// Or importing env in shared files
// lib/config.ts
export const config = {
apiKey: process.env.API_KEY, // Bundled into client!
dbUrl: process.env.DATABASE_URL,
}
```
## Good Example: Validated Environment Configuration
```tsx
// lib/env.server.ts
import { z } from 'zod'
const envSchema = z.object({
// Required
DATABASE_URL: z.string().url(),
SESSION_SECRET: z.string().min(32),
// API Keys
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
// Optional with defaults
NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
// Optional
SENTRY_DSN: z.string().url().optional(),
})
export type Env = z.infer<typeof envSchema>
function validateEnv(): Env {
const parsed = envSchema.safeParse(process.env)
if (!parsed.success) {
console.error('Invalid environment variables:')
console.error(parsed.error.flatten().fieldErrors)
throw new Error('Invalid environment configuration')
}
return parsed.data
}
// Validate once at startup
export const env = validateEnv()
// Usage in server functions
export const getPaymentIntent = createServerFn({ method: 'POST' })
.handler(async () => {
const stripe = new Stripe(env.STRIPE_SECRET_KEY)
// Type-safe, validated access
})
```
## Good Example: Public vs Private Config
```tsx
// lib/env.server.ts - Server only (secrets)
export const serverEnv = {
databaseUrl: process.env.DATABASE_URL!,
sessionSecret: process.env.SESSION_SECRET!,
stripeSecretKey: process.env.STRIPE_SECRET_KEY!,
}
// lib/env.ts - Public config (safe for client)
export const publicEnv = {
appUrl: process.env.VITE_APP_URL ?? 'http://localhost:3000',
stripePublicKey: process.env.VITE_STRIPE_PUBLIC_KEY!,
sentryDsn: process.env.VITE_SENTRY_DSN,
}
// Vite exposes VITE_ prefixed vars to client
// Non-prefixed vars are server-only
```
## Good Example: Environment-Specific Behavior
```tsx
// lib/env.server.ts
export const env = validateEnv()
export const isDevelopment = env.NODE_ENV === 'development'
export const isProduction = env.NODE_ENV === 'production'
export const isStaging = env.NODE_ENV === 'staging'
// lib/logger.server.ts
import { env, isDevelopment } from './env.server'
export function log(level: string, message: string, data?: unknown) {
if (isDevelopment) {
console.log(`[${level}]`, message, data)
return
}
// Production: send to logging service
if (env.SENTRY_DSN) {
// Send to Sentry
}
}
// Server function with environment checks
export const debugInfo = createServerFn()
.handler(async () => {
if (isProduction) {
throw new Error('Debug endpoint not available in production')
}
return {
nodeVersion: process.version,
env: env.NODE_ENV,
}
})
```
## Good Example: Feature Flags via Environment
```tsx
// lib/features.server.ts
import { env } from './env.server'
export const features = {
newCheckout: env.FEATURE_NEW_CHECKOUT === 'true',
betaDashboard: env.FEATURE_BETA_DASHBOARD === 'true',
aiAssistant: env.FEATURE_AI_ASSISTANT === 'true',
}
// Usage in server functions
export const getCheckoutUrl = createServerFn()
.handler(async () => {
if (features.newCheckout) {
return '/checkout/v2'
}
return '/checkout'
})
// Usage in loaders
export const Route = createFileRoute('/dashboard')({
loader: async () => {
return {
showBetaFeatures: features.betaDashboard,
}
},
})
```
## Good Example: Type-Safe env.d.ts
```tsx
// env.d.ts - TypeScript declarations for env vars
declare namespace NodeJS {
interface ProcessEnv {
// Required
DATABASE_URL: string
SESSION_SECRET: string
// Optional
NODE_ENV?: 'development' | 'staging' | 'production'
SENTRY_DSN?: string
// Vite public vars
VITE_APP_URL?: string
VITE_STRIPE_PUBLIC_KEY: string
}
}
```
## Environment Variable Checklist
| Variable | Prefix | Accessible On |
|----------|--------|---------------|
| `DATABASE_URL` | None | Server only |
| `SESSION_SECRET` | None | Server only |
| `STRIPE_SECRET_KEY` | None | Server only |
| `VITE_APP_URL` | `VITE_` | Server + Client |
| `VITE_STRIPE_PUBLIC_KEY` | `VITE_` | Server + Client |
## Context
- Never import `.server.ts` files in client code
- Use `VITE_` prefix for client-accessible variables
- Validate at startup to fail fast on misconfiguration
- Use Zod or similar for runtime validation
- Keep secrets out of error messages and logs
- Consider using `.env.local` for local overrides (gitignored)