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

effect-env

v0.4.1

Published

Typed, testable, policy-aware env management for Effect apps

Readme

effect-env — typed, testable, policy-aware env for Effect apps

npm version GitHub License: MIT

A TypeScript library for managing environment variables with type safety, schema validation, and security features using Effect.

Features

  • Type-safe: Full TypeScript inference from @effect/schema definitions
  • Schema-driven: Validation, transformation, and type inference in one place
  • Server/Client separation: Prevent secrets from leaking to client bundles (t3-env style)
  • Prefix enforcement: Automatic validation that client vars use correct prefix
  • Testable: Built on Effect layers, easy to mock in tests
  • Secure: Redaction helpers for safe logging, production-safe overrides
  • Effect-native: Full composability with Effect ecosystem

Installation

npm install effect-env
# or
bun add effect-env

Quickstart

Simple Usage (Single Schema)

import { Schema as S } from "effect"
import { createSimpleEnv, EnvService } from "effect-env"

// 1. Define your schema
const env = createSimpleEnv(
  S.Struct({
    NODE_ENV: S.Literal("development", "production", "test"),
    PORT: S.NumberFromString,
    DATABASE_URL: S.String
  }),
  process.env
)

// 2. Use in your program
const program = Effect.gen(function* () {
  const envService = yield* EnvService
  const port = yield* envService.get("PORT") // typed as number
  const dbUrl = yield* envService.get("DATABASE_URL") // typed as string
  return { port, dbUrl }
})

// 3. Run with the env layer
Effect.runPromise(Effect.provide(program, env))

Server/Client Separation (t3-env style)

import { Schema as S } from "effect"
import { createEnv, EnvService } from "effect-env"

const env = createEnv({
  // Server-only variables (never exposed to client)
  server: S.Struct({
    DATABASE_URL: S.String,
    API_SECRET: S.String,
    JWT_SECRET: S.String
  }),

  // Client-safe variables (sent to browser)
  // Must start with the clientPrefix!
  client: S.Struct({
    PUBLIC_API_URL: S.String,
    PUBLIC_APP_NAME: S.String
  }),

  clientPrefix: "PUBLIC_",
  runtimeEnv: process.env
})

const program = Effect.gen(function* () {
  const envService = yield* EnvService

  // All typed correctly and safely
  const apiUrl = yield* envService.get("PUBLIC_API_URL")
  const secret = yield* envService.get("API_SECRET")

  return { apiUrl, secret }
})

Effect.runPromise(Effect.provide(program, env))

Schema Transformations

Use Effect Schema's built-in transformations:

const env = createSimpleEnv(
  S.Struct({
    PORT: S.NumberFromString,           // String → Number
    DEBUG: S.BooleanFromString,         // "true" | "false" → boolean
    CONFIG: S.parseJson(S.Unknown),     // JSON string → object
    LOG_LEVEL: S.optionalWith(S.String, {
      default: () => "info"             // Defaults
    })
  })
)

Testing with Override

it("uses overridden PORT", async () => {
  const program = Effect.gen(function* () {
    const env = yield* EnvService

    // Override just for this effect
    return yield* env.withOverride("PORT", "9000")(
      env.get("PORT")
    )
  })

  const result = await Effect.runPromise(Effect.provide(program, env))
  expect(result).toBe("9000")
})

// Note: withOverride is disabled in production (process.env.NODE_ENV === "production")

Validation

Validate environment at startup for clear error reporting:

import { validate } from "effect-env"

const envSchema = S.Struct({
  PORT: S.NumberFromString,
  API_KEY: S.String
})

// In dev/test: prints friendly table and continues
// In production: fails fast with exit code
await Effect.runPromise(validate(envSchema, process.env))

Sample validation report:

Key          | Status       | Details
-------------|--------------|--------
API_KEY      | missing      | required but not provided
PORT         | invalid      | Expected number, actual "abc"

Redaction

Safely log environment variables without exposing secrets:

import { redact } from "effect-env"

const safeEnv = redact(process.env)
// { NODE_ENV: "development", API_KEY: "***", DB_PASSWORD: "***" }

// Custom matchers
const safeEnv = redact(process.env, {
  extra: ["SESSION_ID", /^CUSTOM_/]
})

Redacts keys containing (case-insensitive): key, token, secret, password, pwd, private, bearer, api, auth.

Testing

Test with createSimpleEnv or createEnv using a test record:

import { createSimpleEnv, EnvService } from "effect-env"
import { describe, it, expect } from "vitest"

const testEnv = createSimpleEnv(
  S.Struct({
    PORT: S.NumberFromString,
    DATABASE_URL: S.String
  }),
  {
    PORT: "3000",
    DATABASE_URL: "postgres://localhost"
  }
)

it("reads typed env vars", async () => {
  const program = Effect.gen(function* () {
    const env = yield* EnvService
    return yield* env.get("PORT") // 3000
  })

  const result = await Effect.runPromise(Effect.provide(program, testEnv))
  expect(result).toBe(3000)
})

Or with withOverride in dev/test:

const program = Effect.gen(function* () {
  const env = yield* EnvService
  return yield* env.withOverride("PORT", "8080")(env.get("PORT"))
})

const result = await Effect.runPromise(Effect.provide(program, env))
expect(result).toBe("8080")

API Reference

createEnv(config)

Create a typed environment layer with server/client separation.

createEnv({
  server: S.Schema<Server>,              // Server-only variables
  client: S.Schema<Client>,              // Client-safe variables
  clientPrefix: "PUBLIC_",               // Prefix for client vars
  runtimeEnv?: Record<string, string>,   // Default: process.env
  skipValidation?: boolean,              // Default: false
  onValidationError?: (error) => void    // Custom error handler
}): Layer<Env<Server & Client>>

createSimpleEnv(schema, runtimeEnv?, skipValidation?, onValidationError?)

Create a simple typed environment layer (no server/client separation).

createSimpleEnv(
  schema: S.Schema<T>,                   // Environment schema
  runtimeEnv?: Record<string, string>,   // Default: process.env
  skipValidation?: boolean,              // Default: false
  onValidationError?: (error) => void    // Custom error handler
): Layer<Env<T>>

EnvService Methods

  • get<K>(key: K): Effect<E[K], EnvError> - Get typed value
  • require<K>(key: K): Effect<NonNullable<E[K]>, MissingVarError> - Require non-null
  • all(): Effect<E> - Get all values
  • withOverride<K>(key: K, value: string)(fa: Effect<A>): Effect<A> - Override for testing (disabled in production)

Utilities

  • validate(schema: S.Schema<E>, source: Record<string, string | undefined>, opts?): Effect<void, ValidationError> - Startup validation
  • redact(record: Record<string, string | undefined>, opts?): Record<string, string | undefined> - Safe logging

Legacy APIs (deprecated)

  • fromProcess, fromDotenv, fromRecord - Use createSimpleEnv instead
  • makeEnvSchema - Wrapper no longer needed with direct Schema usage

Notes

  • Security: Never log raw env vars. Use redact() for safe logging.
  • Type Inference: All types flow through the schema; no additional type annotations needed.
  • Defaults: Use S.optionalWith(S.String, { default: () => "value" }) for defaults.
  • Errors: Clear messages include key names and value snippets for debugging.
  • Production: Validation fails fast; withOverride is disabled.
  • Server/Client: Client variables MUST start with the configured prefix (default "PUBLIC_").

Contributing

PRs welcome! Run npm test and npm run typecheck before submitting.

License

MIT