Environment variables
createEnv defines a Zod-validated environment schema with automatic public/private separation, an HTTP endpoint to expose public variables (with ETag caching), a React hook, and type generation. This is the recommended way to read config and secrets in your app — reference the typed env.X everywhere instead of process.env.
Under the hood it reads through the runtime-safe primitives readEnv, isProduction, and isDebugEnabled (so it works on Workers too — Covara itself never touches process.env directly). Reach for those primitives directly only for a quick one-off read where a full schema is overkill; otherwise prefer createEnv for type safety and fail-fast validation.
Define a schema
import { createEnv } from "covara";
import { z } from "zod";
export const env = createEnv({
NODE_ENV: z.enum(["development", "production", "test"]),
PORT: z.string().transform(Number),
DATABASE_URL: z.string().url(),
PUBLIC_API_URL: z.string().url(),
PUBLIC_VERSION: z.string(),
});
env.PORT; // number
env.DATABASE_URL; // string
Values are read from process.env using the key path joined with underscores. Nested schemas map accordingly:
const env = createEnv({
SERVER: { PORT: z.string().transform(Number), HOST: z.string() },
DB: { URL: z.string(), POOL_SIZE: z.string().transform(Number) },
});
// reads SERVER_PORT, SERVER_HOST, DB_URL, DB_POOL_SIZE
Use Zod defaults and transforms for optional/parsed values:
createEnv({
PORT: z.string().default("3000").transform(Number),
DEBUG: z.string().default("false").transform((v) => v === "true"),
ALLOWED_ORIGINS: z.string().transform((s) => s.split(",")),
});
Public vs private
A variable is public (exposable to the client) in two ways:
import { createEnv, envVariable } from "covara";
createEnv({
PUBLIC_API_URL: z.string(), // PUBLIC_ prefix → automatically public
SECRET_KEY: z.string(), // private by default
API_URL: envVariable(process.env.API_URL, z.string(), { public: true }), // explicit
});
Everything else is private. env.getPublicEnvironmentVariables() returns only the public set.
Serving public vars over HTTP
import { usePublicEnv } from "covara";
app.route("/api/env", usePublicEnv(env, {
cacheControl: "public, max-age=3600",
exposeSchema: true, // also serve /api/env/schema for typegen
}));
This mounts GET /api/env (public vars as JSON) and GET /api/env/schema (schema for typegen). The endpoint computes an ETag from the public values at startup, so If-None-Match returns 304 until the server restarts with new values.
Client
import { fetchPublicEnv, createEnvClient } from "covara/client";
const env = await fetchPublicEnv<{ PUBLIC_API_URL: string }>("http://localhost:3000");
const envClient = createEnvClient<{ PUBLIC_API_URL: string }>({ baseUrl: "http://localhost:3000" });
const unsubscribe = envClient.subscribe((env) => console.log(env), 60000); // poll
import { usePublicEnv } from "covara/client/react";
function App() {
const { env, isLoading, error, refetch } = usePublicEnv<{ PUBLIC_API_URL: string; PUBLIC_VERSION: string }>();
if (isLoading) return <div>Loading…</div>;
return <p>{env?.PUBLIC_API_URL} v{env?.PUBLIC_VERSION}</p>;
}
Hook options: baseUrl, envPath (default /api/env), refreshInterval, enabled.
Type generation
The typegen tool emits a PublicEnv type from the schema endpoint:
export type PublicEnv = { PUBLIC_API_URL: string; PUBLIC_VERSION: string };
npx covara types --url http://localhost:3000 --out src/generated/api-types.ts
Best practices
- Use the
PUBLIC_prefix consistently; never expose secrets (onlyPUBLIC_/{ public: true }are exposed). - Validate strictly with Zod (
.url(),.min(),.pipe()). - Cache aggressively on the client with
cacheControl.
See the environment-variables contract for the public-exposure guarantee.