The schema lives in Envless
Every variable in the dashboard has a type, a required/optional flag, and a visibility. envless reads that schema and generates a TypeScript declaration file. You don’t repeat it in code.
Dashboard field Effect in code Name Becomes a property on env Type (string, number, boolean) Becomes the property type and triggers coercion Required Required-ness in the type, throws if missing Default Used when the value isn’t set Visibility (server / client) Determines which import surface includes it
Generate types
Writes envless-env.d.ts next to your package.json:
// envless-env.d.ts (generated)
declare module '@goenvless/envless/server' {
export const env : {
DATABASE_URL : string ;
PORT : number ;
STRIPE_SECRET : string ;
FEATURE_X_ENABLED : boolean ;
NEXT_PUBLIC_API_URL : string ;
};
}
declare module '@goenvless/envless/client' {
export const env : {
NEXT_PUBLIC_API_URL : string ;
};
}
Commit this file. It contains no secrets, only the shape — and committing it means teammates get autocomplete without running anything.
Keep types in sync
Run envless types whenever you add or rename a variable in the dashboard. Or wire it up so it runs automatically:
package.json
.github/workflows/ci.yml
{
"scripts" : {
"postinstall" : "envless types" ,
"dev" : "envless types && next dev"
}
}
Coercion
Values are stored as strings in the encrypted bundle. The proxy coerces them on read based on the schema type:
env . PORT // number — "3000" → 3000
env . FEATURE_X // boolean — "true" → true
env . DATABASE_URL // string
Coercion happens once and is cached. There’s no Number(env.PORT) ceremony.
Missing variables throw loudly
env . NOT_A_REAL_KEY // throws: "Missing env var: NOT_A_REAL_KEY (env: development)"
This is intentional — silent undefined is the single worst thing about process.env. If you genuinely want optional, mark the variable optional in the dashboard and the type becomes string | undefined.
Test overrides
For tests, you can override values without mutating global state:
import { env , __override } from '@goenvless/envless/server' ;
test ( 'feature X enabled' , () => {
using _ = __override ({ FEATURE_X_ENABLED: true });
expect ( env . FEATURE_X_ENABLED ). toBe ( true );
});
The using declaration scopes the override to the block — no cleanup needed.
Why this beats t3-env / hand-rolled Zod
Hand-rolled validation envlessSchema location Lives in code, drifts from .env Lives in the dashboard, single source Add a variable Edit env.ts, edit .env, edit .env.example, commit Add it in the dashboard, run envless types Required/default Re-declared in Zod Already in the schema Client/server split Manual client: / server: blocks Visibility flag in dashboard Runtime values Manually mapped via runtimeEnv: { ... } Automatic