npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@foxen/env

v1.4.1

Published

Environment variable management for Foxen - loading, validation, type-safe schemas, and runtime access

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/typebox

Quick 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):

  1. .env - Base environment
  2. .env.local - Local overrides (not in test mode)
  3. .env.[mode] - Mode-specific (.env.development, .env.production, .env.test)
  4. .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 everything

env.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(): TObject

Generation

// 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): boolean

Loading

// 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): string

Integration 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 dev

The CLI will automatically:

  1. Load .env files based on NODE_ENV
  2. Inject variables into process.env
  3. Regenerate schema files when .env changes

Best Practices

  1. Add .env.local to .gitignore - Contains local secrets
  2. Create .env.example - Document required variables
  3. Use prefix for app-specific vars - APP_, MYAPP_
  4. Run generation in CI - Catch missing vars early
  5. 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