---
title: Environment
description: "How settings and secret keys work in zap.ts: one schema per app, your values in .env.local, and one check that tells you what is missing."
---

An environment variable is a setting your app reads when it starts. A database
address or an API key is an environment variable.

zap.ts manages them with [varlock](https://varlock.dev). Varlock reads a file
that lists the variables, checks their values, and writes the TypeScript types
for you.

## One schema per app

A `.env.schema` file declares variables. It says the name, the type, whether
the value is needed, and whether the value is safe to send to the browser.

The schema holds no real secret. The values in it are safe defaults for your
own computer.

| Schema                          | Holds                                                                               | Read by            |
| ------------------------------- | ----------------------------------------------------------------------------------- | ------------------ |
| `.env.schema`                   | The shared keys: Better Auth, Google, Stripe, OpenAI, mail, PostHog, Sentry, `R2_*` | `packages/*`       |
| `apps/web/.env.schema`          | The shared keys, plus `API_URL`                                                     | `apps/web`         |
| `apps/admin/.env.schema`        | The shared keys                                                                     | `apps/admin`       |
| `apps/marketing/.env.schema`    | `SITE_URL`, `APP_URL`, plus the PostHog and Sentry keys                             | `apps/marketing`   |
| `apps/api/.env.schema`          | `CORS_ORIGINS`, `API_URL`, plus `SENTRY_*` and the keys it needs                    | `apps/api`         |
| `packages/database/.env.schema` | `DATABASE_URL`                                                                      | The `db:*` scripts |

:::note
`DATABASE_URL` lives only in `packages/database/.env.schema`. It is not in the
root schema. The database package owns it, and the `db:*` scripts read it from
there.
:::

## How an app borrows a variable

A variable has one owner. Other schemas borrow it with `@import()`.

`apps/web/.env.schema` takes everything from the root schema:

```bash
# @import(../../.env.schema)
```

`apps/marketing/.env.schema` takes only six variables, by name:

```bash
# @import(../../.env.schema, pick=[POSTHOG_KEY, POSTHOG_HOST, SENTRY_DSN, SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN])
```

You can also pick by prefix. A `*` at the end takes every name that starts
that way. `apps/api/.env.schema` uses this:

```bash
# @import(../../.env.schema, pick=[SENTRY_*, BETTER_AUTH_*, GOOGLE_*, STRIPE_*])
```

`SENTRY_*` takes `SENTRY_DSN`, `SENTRY_ORG`, `SENTRY_PROJECT` and
`SENTRY_AUTH_TOKEN` in one line.

This way a key is declared once. When you change its type or its default, every
app that borrows it changes too.

## Putting in your own values

Real values go in a file named `.env.local`.

Place that file next to the schema that declares the variable. So
`DATABASE_URL` goes in `packages/database/.env.local`, because that is where
the schema declares it.

```bash
# packages/database/.env.local
DATABASE_URL=postgresql://user:password@your-host:5432/your-database
```

`.env.local` files are never saved to git. Your keys stay on your computer.

You do not need one to start. The defaults in the schema point at the services
that `docker compose up -d` runs for you.

## Checking what your app will read

Run one of these to see how every value resolves:

```bash
pnpm run env:web
pnpm run env:admin
pnpm run env:marketing
pnpm run env:api
pnpm run env:packages
```

The first four print every variable of that app, its value, and the file the
value came from. So you can see whether your `.env.local` was picked up.

`pnpm run env:packages` is different. It does not print a list. It writes the
TypeScript types again for the shared schema, so your editor knows the new
variable.

## When a value is missing

A variable marked `@required` must have a value.

If one is missing, the build stops. It does not start half broken. The error
names the variable, so you know which line to fix.

The same check runs when the dev server starts.

## Secrets stay secret

Each variable says whether it is sensitive. In zap.ts, sensitive is the
default.

A sensitive value never goes into the code the browser downloads. Varlock
leaves it out of the bundle.

A sensitive value is also hidden in logs. If your code prints one by mistake,
you see a redacted placeholder, not the key.

A variable that is safe to show is marked `@sensitive=false`. `POSTHOG_HOST`
and `SITE_URL` are two examples. They are public addresses, so there is no
harm.

## Builds in CI

CI is the server that builds your code after you push it. It has no secret
keys, and it should not need any.

So `apps/web` and `apps/admin` each hold a file named `.env.ci`. It contains
placeholder values, one for every required variable.

Varlock loads that file when the `VARLOCK_ENV` variable is set to `ci`:

```bash
VARLOCK_ENV=ci pnpm run build
```

The build then passes its checks with no real key anywhere.

## Values on a deploy

A Cloudflare deploy runs through `varlock-wrangler`. It reads your schema and
sends the values to Cloudflare for you.

Values that are not sensitive go up as Cloudflare vars. Sensitive values go up
as Cloudflare secrets.

:::warning
Varlock owns those values. If you add one by hand in the Cloudflare dashboard,
the next deploy removes it. Add it to your schema and your `.env.local`
instead.
:::

A Vercel deploy runs through `varlock run`, which resolves the same values for
the build but sends nothing anywhere. You put the values in Vercel yourself,
with `vercel env add` or in the project settings. Your schema stays the source
of truth for what an app needs; on Vercel it is not the source of truth for
what the host holds. See [Deployment](/guides/deployment).

## Reading a variable in your code

You do not read `process.env` directly. You import a typed object instead.

The [Environment](/packages/environment) page covers that part: the `env`
object and the `requireEnv` helper.

## Related

- [Environment package](/packages/environment) — reading values in code
- [Quickstart](/quickstart) — the first run, with no keys
- [Local development](/guides/local-development) — the services the defaults point at
- [Database](/packages/database) — where `DATABASE_URL` is used
- [API](/apps/api) — `CORS_ORIGINS` and `API_URL`
