Environment Variables
Declare, validate, and read environment variables with typebase/env.ts.
Last updated on
typebase/env.ts is where you declare the environment variables your server needs. Typebase validates them when the server boots, and hands them to your handlers fully typed on env.
import { defineEnv } from 'typebase-io/server';
import { z } from 'zod';
export const env = defineEnv({
RESEND_API_KEY: z.string().min(1),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
});npx typebase-io-cli init scaffolds this file for you with an empty schema and a commented-out example. If your project doesn't have one, create it yourself; nothing else needs to change.
defineEnv accepts any Standard Schema library, the same as .input() and .output() on actions. Zod, Valibot, and
ArkType all work. Use whichever one the rest of your project already uses.
Reading them in an action
Declared variables arrive on the handler context as env:
import { z } from 'zod';
import { action } from '../../_generated/server.ts';
export const send = action.input(z.object({ to: z.email() })).handler(async ({ env, input }) => {
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: { Authorization: `Bearer ${env.RESEND_API_KEY}` },
body: JSON.stringify({ to: input.to }),
});
return { ok: true };
});env.RESEND_API_KEY is a string, not string | undefined, because the server refuses to start when it's missing. No ! assertions, no ?? '' fallbacks.
What Typebase adds for you
Two variables are managed by Typebase and appear on env without you declaring them:
| Variable | Added when |
|---|---|
DATABASE_URL | your project has a db/schema.ts |
BETTER_AUTH_SECRET | your project has an auth.ts |
They're validated and typed exactly like your own keys, so env.DATABASE_URL is available in any handler of a project that has a database, even one with no env.ts at all. The CLI already sets both on your provider during deploy, so in practice you never touch them.
Declaring one of them yourself is allowed, and your definition wins:
export const env = defineEnv({
DATABASE_URL: z.string().startsWith('postgres://'),
});Where the values come from
Nothing about env.ts provides values; it only describes what must exist. The values come from the environment the server runs in:
- Deployed: whatever is set on your provider. Add your own with
npx typebase-io-cli env <target> add <KEY> <value>, once per target. - Locally: the
.envfile intypebase/_server/, loaded bydotenv(see Work locally). - Cloudflare Workers: read from the Worker's bindings rather than
process.env, so secrets set on the Worker work without any extra wiring.
The two directions are independent, and neither implies the other. env add uploads a value to your provider; it never edits typebase/env.ts. Declaring a key here never sets its value anywhere. A new variable normally needs both: declare it in this file so it's typed and validated, and set its value for each target you deploy to.
When a variable is missing
The generated server validates the whole schema before it serves a single request, so a missing or malformed variable is a boot failure rather than an undefined surfacing deep inside a handler later:
✗ Typebase server failed to start.
Invalid environment variables:
RESEND_API_KEY Required
Set them with:
npx typebase-io-cli env prod add <KEY> <value>Set the variable and redeploy (or restart, if you're running locally).
Options
defineEnv takes an optional second argument:
export const env = defineEnv(
{
RESEND_API_KEY: z.string().min(1),
},
{
emptyStringAsUndefined: true,
skipValidation: process.env.SKIP_ENV_VALIDATION === 'true',
}
);| Option | Description | Default |
|---|---|---|
emptyStringAsUndefined | Treat KEY= as missing instead of as an empty string. | true |
skipValidation | Skip validation entirely. Useful for builds that import your server without running it. | false |
Both are carried into the generated server as written, so an expression like the one above is evaluated at boot on the server, not at generation time.
Undeclared variables
process.env.X still works anywhere inside typebase/, and is the right escape hatch for something optional or trivial:
const region = process.env.AWS_REGION ?? 'us-east-1';You lose the boot-time check and the type, and on Cloudflare Workers process.env is not the runtime's own source of configuration, so prefer env.ts for anything the server genuinely can't run without.
Regenerating types
Adding or removing env.ts changes the handler context, so run codegen afterwards:
npx typebase-io-cli codegenEditing the contents of an existing env.ts (adding or removing a key) does not need a codegen run. npx typebase-io-cli deploy reruns codegen on every deploy.