Environment Variables
This guide covers environment variable management in EreoJS.
Basics
EreoJS loads environment variables from .env files automatically.
File Loading Order
.env- Base variables (committed).env.local- Local overrides (gitignored).env.{mode}- Mode-specific (e.g.,.env.production).env.{mode}.local- Mode-specific local (gitignored)
Later files override earlier ones.
Example Files
bash
# .env - Base configuration
DATABASE_URL=postgres://localhost/myapp_dev
API_URL=http://localhost:3001
# .env.local - Local secrets (gitignored)
SESSION_SECRET=my-local-secret
# .env.production - Production settings
DATABASE_URL=postgres://prod-server/myapp
API_URL=https://api.example.com
# .env.production.local - Production secrets (gitignored)
SESSION_SECRET=super-secret-production-keyType-Safe Environment
Define a Schema
ts
// src/lib/env.ts
import { env, setupEnv, initializeEnv } from '@ereo/core'
export const envSchema = {
// Required variables
DATABASE_URL: env.string(),
SESSION_SECRET: env.string().validate(s => s.length >= 32),
// With defaults
PORT: env.port().default(3000),
NODE_ENV: env.enum(['development', 'production', 'test']).default('development'),
// Optional
SENTRY_DSN: env.url().optional(),
// Public (exposed to client)
PUBLIC_API_URL: env.string().public(),
PUBLIC_APP_NAME: env.string().default('My App').public(),
// Complex types
FEATURE_FLAGS: env.json<{ beta: boolean }>().default({ beta: false }),
ALLOWED_ORIGINS: env.array().default([])
}
export type Env = typeof envSchemaInitialize Environment
ts
// src/index.ts
import { setupEnv, initializeEnv } from '@ereo/core'
import { envSchema } from './lib/env'
async function main() {
// Validate and load environment
const result = await setupEnv('.', envSchema, process.env.NODE_ENV)
if (!result.success) {
console.error('Environment validation failed:')
result.errors.forEach(e => console.error(` ${e.key}: ${e.message}`))
process.exit(1)
}
// Make available globally
initializeEnv(result.data)
// Now start the app
// ...
}
main()Accessing Variables
ts
import { getEnv, requireEnv } from '@ereo/core'
// Optional access
const sentryDsn = getEnv<string>('SENTRY_DSN')
// Required access (throws if missing)
const dbUrl = requireEnv<string>('DATABASE_URL')
// In components/routes
export const loader = createLoader(async () => {
const apiUrl = requireEnv<string>('PUBLIC_API_URL')
const data = await fetch(`${apiUrl}/posts`)
return { posts: await data.json() }
})Public vs Private Variables
Private Variables
Only available on the server:
ts
// Only in loaders, actions, API routes
const dbUrl = requireEnv<string>('DATABASE_URL')
const secret = requireEnv<string>('SESSION_SECRET')Public Variables
Available on both server and client:
ts
// Schema
PUBLIC_API_URL: env.string().public()
// Usage - works everywhere
const apiUrl = requireEnv<string>('PUBLIC_API_URL')Convention: Prefix with PUBLIC_ for clarity.
Exposing to Client
tsx
// routes/_layout.tsx
import { getPublicEnv } from '@ereo/core'
import { envSchema } from '../lib/env'
export const loader = createLoader(async () => {
const publicEnv = getPublicEnv(envSchema)
return { env: publicEnv }
})
export default function Layout({ children, loaderData }) {
return (
<html>
<head>
<script
dangerouslySetInnerHTML={{
__html: `window.__ENV__ = ${JSON.stringify(loaderData.env)}`
}}
/>
</head>
<body>{children}</body>
</html>
)
}Access in islands:
tsx
// islands/SomeComponent.tsx
function getPublicEnv(key: string) {
if (typeof window !== 'undefined') {
return window.__ENV__?.[key]
}
return process.env[key]
}
export default function SomeComponent() {
const apiUrl = getPublicEnv('PUBLIC_API_URL')
// ...
}Schema Types
String
ts
DATABASE_URL: env.string()Number
ts
MAX_ITEMS: env.number().default(100)Boolean
ts
DEBUG: env.boolean().default(false)
// Accepts: true, false, 1, 0, yes, noPort
ts
PORT: env.port().default(3000)
// Validates 1-65535URL
ts
API_URL: env.url()
// Validates URL formatEnum
ts
LOG_LEVEL: env.enum(['debug', 'info', 'warn', 'error']).default('info')Array
ts
ALLOWED_ORIGINS: env.array().default([])
// Parses comma-separated: "a,b,c" → ["a", "b", "c"]JSON
ts
FEATURE_FLAGS: env.json<{ beta: boolean; newUI: boolean }>()
// Parses JSON stringValidation
Required vs Optional
ts
// Required (throws validation error if missing)
DATABASE_URL: env.string().required()
// Optional (undefined if missing, no error)
SENTRY_DSN: env.string()
// With default (uses default if missing)
PORT: env.port().default(3000)Note: Variables are optional by default. Use .required() to make them mandatory.
Custom Validation
ts
// Minimum length (return true for valid, or error message string)
SESSION_SECRET: env.string()
.required()
.validate(s => s.length >= 32 || 'Must be at least 32 characters')
// Pattern matching
API_KEY: env.string()
.required()
.validate(s => /^sk_/.test(s) || 'Must start with sk_')
// PostgreSQL connection check
DATABASE_URL: env.string()
.required()
.validate(s => s.startsWith('postgres://') || 'Must be a PostgreSQL connection string')Note: The validate function should return true for valid values, or a string error message for invalid values.
Generate TypeScript Types
ts
import { generateEnvTypes } from '@ereo/core'
import { envSchema } from './lib/env'
const types = generateEnvTypes(envSchema)
await Bun.write('src/env.d.ts', types)Output:
ts
// src/env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string
SESSION_SECRET: string
PORT?: string
NODE_ENV?: 'development' | 'production' | 'test'
PUBLIC_API_URL: string
// ...
}
}Best Practices
- Never commit secrets - Add
.env.localto.gitignore - Use PUBLIC_ prefix - For client-exposed variables
- Validate early - Fail fast on missing/invalid config
- Type your env - Use schema for type safety
- Document variables - List required vars in README
- Use defaults wisely - Development defaults, required in production
- Rotate secrets - Change production secrets periodically
Example .gitignore
gitignore
# Environment files
.env.local
.env.*.local
.env.production
# Keep .env and .env.development as templates
!.env
!.env.development