@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 theVITE_PUBLIC_prefix.createEnv()-- for Next.js projects. Client variables must use theNEXT_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:
Variables defined here are only available on the server. If accessed on the client, an EnvError is thrown at runtime.
Variables defined here must be prefixed with VITE_PUBLIC_ (Vite) or NEXT_PUBLIC_ (Next.js). They are available in both server and client code.
Optional. Variables defined in shared are available in both environments without requiring a prefix.
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:
| Preset | Description |
|---|---|
vercel | Vercel system environment variables |
neonVercel | Neon PostgreSQL on Vercel |
render | Render platform variables |
railway | Railway platform variables |
fly | Fly.io platform variables |
netlify | Netlify platform variables |
coolify | Coolify platform variables |
vite | Built-in Vite variables (BASE_URL, MODE, DEV, etc.) |
wxt | WXT browser extension variables |
upstashRedis | Upstash Redis connection variables |
uploadthing | UploadThing file upload token |
supabaseVercel | Supabase 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:
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.