@foxen/env
v1.4.1
Published
Environment variable management for Foxen - loading, validation, type-safe schemas, and runtime access
Maintainers
Readme
@foxen/env
Environment variable management for Foxen applications with Next.js-style .env file loading, automatic type inference, and TypeBox schema generation for validation.
Installation
bun add @foxen/env @sinclair/typeboxQuick Start
Basic Usage
import { bootstrapEnv, env } from '@foxen/env';
// At app startup
bootstrapEnv();
// Access env vars (typed based on inference)
console.log(env.DATABASE_URL); // string
console.log(env.PORT); // number (if value was "3000")
console.log(env.DEBUG); // boolean (if value was "true")With Code Generation (Recommended)
Generate type-safe schema files for full TypeScript support:
// scripts/generate-env.ts
import { generateAndWriteEnvFiles } from '@foxen/env';
await generateAndWriteEnvFiles({
rootDir: process.cwd(),
outputDir: '.foxen',
});Then use the generated files:
// src/index.ts
import { bootstrapEnv } from '@foxen/env';
import { EnvSchema, env } from './.foxen';
import type { Env } from './.foxen';
// Bootstrap with schema validation
bootstrapEnv({}, EnvSchema);
// Full type safety!
const port: number = env.PORT;
const debug: boolean = env.DEBUG;.env File Hierarchy
Files are loaded in this order (later overrides earlier):
.env- Base environment.env.local- Local overrides (not in test mode).env.[mode]- Mode-specific (.env.development,.env.production,.env.test).env.[mode].local- Mode-specific local (not in test mode)
This matches Next.js behavior exactly.
Configuration
import { bootstrapEnv, generateEnvFiles } from '@foxen/env';
// Bootstrap options
bootstrapEnv({
rootDir: process.cwd(), // Where to look for .env files
mode: 'development', // 'development' | 'test' | 'production'
strict: false, // Throw on validation errors
validate: true, // Validate against schema
injectToProcessEnv: true, // Inject into process.env
});
// Generation options
await generateEnvFiles({
rootDir: process.cwd(),
outputDir: '.foxen',
prefix: 'APP_', // Only include vars starting with prefix
stripPrefix: true, // Remove prefix in generated types
exclude: ['SECRET_KEY'], // Variables to exclude
typeOverrides: { // Override inferred types
PORT: 'integer',
DEBUG: 'boolean',
},
});Type Inference
Values are automatically typed based on their content:
| Value | Inferred Type | TypeScript Type |
|-------|---------------|-----------------|
| true, false, yes, no, on, off, 1, 0 | boolean | boolean |
| -?\d+ (integers) | integer | number |
| -?\d+\.\d+ (decimals) | number | number |
| Everything else | string | string |
Override with typeOverrides when needed:
await generateEnvFiles({
typeOverrides: {
PORT: 'string', // Keep as string even though it looks like a number
},
});Generated Files
When you run code generation, these files are created:
.foxen/
├── env.schema.ts # TypeBox schema for validation
├── env.d.ts # ProcessEnv type augmentation
├── env.types.ts # Env interface definition
├── env.runtime.ts # Runtime env accessor
└── index.ts # Re-exports everythingenv.schema.ts
import { Type, type Static } from "@sinclair/typebox";
export const EnvSchema = Type.Object({
DATABASE_URL: Type.String(),
PORT: Type.Transform(Type.String())
.Decode((v) => parseInt(v, 10))
.Encode((v) => String(v)),
DEBUG: Type.Transform(Type.String())
.Decode((v) => v === "true")
.Encode((v) => String(v)),
});
export type EnvSchemaType = Static<typeof EnvSchema>;env.d.ts
declare global {
namespace NodeJS {
interface ProcessEnv {
readonly DATABASE_URL: string;
readonly PORT: string;
readonly DEBUG: string;
}
}
}
export {};env.types.ts
export interface Env {
readonly DATABASE_URL: string;
readonly PORT: number;
readonly DEBUG: boolean;
}
export type EnvKey = keyof Env;API Reference
Bootstrap & Lifecycle
// Load .env files
bootstrapEnv(options?: BootstrapOptions, schema?: TObject): void
// Reset state (for testing)
resetEnv(): void
// Check if loaded
isEnvLoaded(): boolean
// Get loaded file paths
getLoadedFiles(): readonly string[]Typed Access
// Get single value
getEnv<T>(key: string, defaultValue?: T): T
// Get all decoded values
getAllEnv(): Readonly<Record<string, unknown>>
// Get raw string values
getRawEnv(): Readonly<Record<string, string>>
// Create typed proxy
createEnvProxy<T>(): T
// Default proxy instance
env: Record<string, unknown>Validation
// Validate against schema
validateEnv(schema: TObject): ValidationResult
// Check required variables
checkRequired(required: readonly string[]): ValidationResult
// Build schema dynamically
buildEnvSchema(): TObjectGeneration
// Generate files (returns content)
generateEnvFiles(config?: EnvConfig): Promise<GenerationResult>
// Generate and write to disk
generateAndWriteEnvFiles(config?: EnvConfig): Promise<GenerationResult>
// Check if regeneration needed
needsRegeneration(config?: EnvConfig): booleanLoading
// Load env files
loadEnvFiles(config?: EnvConfig): LoadResult
// Get file hierarchy
getEnvFileHierarchy(mode: EnvMode): readonly string[]
// Get existing files
getExistingEnvFiles(rootDir: string, mode: EnvMode): readonly string[]Parsing
// Parse .env content
parseEnvFile(source: string, filePath?: string): ParsedEnv
// Parse single line
parseEnvLine(line: string, existingEnv?: ParsedEnv): { key: string; value: string } | null
// Convert to .env format
stringifyEnvFile(env: ParsedEnv, options?: StringifyOptions): stringIntegration with Foxen CLI
The @foxen/env package is designed to work seamlessly with the Foxen CLI:
# Generate env files
foxen env generate
# Start dev server (auto-loads env)
foxen devThe CLI will automatically:
- Load
.envfiles based on NODE_ENV - Inject variables into process.env
- Regenerate schema files when
.envchanges
Best Practices
- Add
.env.localto.gitignore- Contains local secrets - Create
.env.example- Document required variables - Use prefix for app-specific vars -
APP_,MYAPP_ - Run generation in CI - Catch missing vars early
- Use strict mode in production - Fail fast on invalid env
Example: Full Setup
// foxen.config.ts
import { defineEnvConfig } from '@foxen/env';
export const envConfig = defineEnvConfig({
prefix: 'APP_',
stripPrefix: true,
validateExample: true,
typeOverrides: {
APP_PORT: 'integer',
APP_RATE_LIMIT: 'integer',
},
});// src/env.ts
import { bootstrapEnv } from '@foxen/env';
import { EnvSchema, env } from '../.foxen';
import type { Env } from '../.foxen';
export function setupEnv(): void {
bootstrapEnv({ strict: process.env.NODE_ENV === 'production' }, EnvSchema);
}
export { env };
export type { Env };// src/index.ts
import { setupEnv, env } from './env';
setupEnv();
console.log(`Starting server on port ${env.PORT}`);License
MIT
