TypebaseTypebase

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.

typebase/env.ts
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:

typebase/actions/mutations/emails.ts
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:

VariableAdded when
DATABASE_URLyour project has a db/schema.ts
BETTER_AUTH_SECRETyour 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:

typebase/env.ts
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 .env file in typebase/_server/, loaded by dotenv (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:

typebase/env.ts
export const env = defineEnv(
  {
    RESEND_API_KEY: z.string().min(1),
  },
  {
    emptyStringAsUndefined: true,
    skipValidation: process.env.SKIP_ENV_VALIDATION === 'true',
  }
);
OptionDescriptionDefault
emptyStringAsUndefinedTreat KEY= as missing instead of as an empty string.true
skipValidationSkip 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 codegen

Editing 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.

On this page