Raypx

@raypx/env

Type-safe environment variable validation with Zod.

@raypx/env provides a framework-agnostic, type-safe way to define and validate environment variables. It uses Standard Schema compliant validators (Zod by default) and enforces server/client variable separation at both the type level and runtime.

Core API

The package exposes two framework-specific factory functions and a low-level defineEnvCore:

  • createViteEnv() -- for Vite / TanStack Start projects. Client variables must use the VITE_PUBLIC_ prefix.
  • createEnv() -- for Next.js projects. Client variables must use the NEXT_PUBLIC_ prefix.
  • defineEnvCore() -- framework-agnostic core, used by both factories above.

Basic Usage

import { createViteEnv } from "@raypx/env"
import { z } from "zod"

export const envs = () =>
  createViteEnv({
    server: {
      DATABASE_URL: z.string().url(),
      AUTH_SECRET: z.string().min(32),
    },
    client: {
      VITE_PUBLIC_APP_URL: z.string().url(),
    },
  })

const env = envs()
// env.DATABASE_URL       -- string (server only)
// env.VITE_PUBLIC_APP_URL -- string (client + server)

Server vs. Client Variables

The server and client keys control which variables are accessible in which environment:

Preset Composition with extends

Packages can export their own env presets and compose them using the extends option. Later configurations override earlier ones.

import { createViteEnv, type Preset } from "@raypx/env"
import { z } from "zod"

export const authEnv = {
  id: "auth",
  server: {
    AUTH_SECRET: z.string().min(32),
    AUTH_URL: z.url(),
  },
  client: {
    VITE_PUBLIC_AUTH_GOOGLE_ID: z.string().optional(),
  },
} as const satisfies Preset

// Consuming app merges presets
const env = createViteEnv({
  extends: [authEnv, emailEnv],
  server: {
    PORT: z.coerce.number().default(3000),
  },
})

Built-in Presets

The package ships with presets for common platforms and services. Each preset is a Preset object that can be passed to extends:

PresetDescription
vercelVercel system environment variables
neonVercelNeon PostgreSQL on Vercel
renderRender platform variables
railwayRailway platform variables
flyFly.io platform variables
netlifyNetlify platform variables
coolifyCoolify platform variables
viteBuilt-in Vite variables (BASE_URL, MODE, DEV, etc.)
wxtWXT browser extension variables
upstashRedisUpstash Redis connection variables
uploadthingUploadThing file upload token
supabaseVercelSupabase on Vercel integration
import { createViteEnv, vercel, neonVercel, vite } from "@raypx/env"

const env = createViteEnv({
  extends: [vercel, neonVercel, vite],
  server: {
    AUTH_SECRET: z.string().min(32),
  },
})

Advanced Options

The skip option bypasses all validation and returns default values. This is useful in testing or CI environments where not all env vars are set.

The onError callback is invoked when validation fails. By default, it logs the issues and throws an EnvError. The onInvalidAccess callback fires when a server variable is accessed on the client.

How @raypx/auth Uses This Package

Each feature package defines its own env file that re-exports a Preset:

packages/auth/src/env.ts
import { createViteEnv, type Preset, z } from "@raypx/env"

export const authEnv = {
  server: {
    AUTH_SECRET: z.string().min(32),
    AUTH_URL: z.url(),
    AUTH_GOOGLE_SECRET: z.string().optional(),
    AUTH_GITHUB_ID: z.string().optional(),
    AUTH_GITHUB_SECRET: z.string().optional(),
  },
  client: {
    VITE_PUBLIC_AUTH_GOOGLE_ID: z.string().optional(),
  },
} as const satisfies Preset

export const envs = () => createViteEnv(authEnv)

This pattern is repeated across @raypx/database, @raypx/email, @raypx/storage, and @raypx/otp.

On this page