# Environment (/docs/ship/environment)

---
title: Environment
description: Two files declare the variables your app reads. QUESTPIE validates them when the app boots, types every read, and writes a browser module holding only the public ones.
kind: guide
package: questpie
---

Your app needs a database URL and a secret. Some values also have to reach the
browser. Where do you put them, and what stops a secret from riding along?

## Two files

Put both beside `questpie.config.ts`. Codegen finds them by name. A `.mts`
extension works too.

`env.client.ts` holds what the browser may see. Key each var by its plain name,
with no prefix.

```ts title="src/questpie/server/env.client.ts"
import { clientEnv } from "questpie/env";
import { z } from "zod";

export default clientEnv({
	consumers: ["vite"],
	vars: {
		APP_URL: z.string().default("http://localhost:3000"),
	},
});
```

`env.ts` holds the server ones. Import the client file and pass it as `client`.

```ts title="src/questpie/server/env.ts"
import { env } from "questpie/env";
import { z } from "zod";

import client from "./env.client";

export default env({
	client,
	server: {
		DATABASE_URL: z.string(),
		BETTER_AUTH_SECRET: z.string().min(32),
		SMTP_PORT: z.coerce.number().default(1025),
	},
});
```

Any validator that implements [Standard Schema](https://standardschema.dev)
works here. The examples use Zod. Then generate:

```bash
questpie generate
```

## What that produced

| Surface       | Where it shows up                                                |
| ------------- | ---------------------------------------------------------------- |
| Server `env`  | The default export of `env.ts`, re-exported from `#questpie`     |
| `app.env`     | The same frozen object on the app instance, typed from your file |
| Client module | `.generated/env.client.vite.ts`, one per consumer you named      |

```ts title="Server code"
import { env } from "#questpie";

env.DATABASE_URL; // string
env.SMTP_PORT; // number
env.APP_URL; // string, client vars are validated on the server too
```

```ts title="Browser code"
import { env } from "#questpie/env.client.vite";

env.APP_URL; // string, inlined from import.meta.env.VITE_APP_URL
```

`questpie.config.ts` is the one exception. The generated index imports the
config, so the config cannot import back. It imports `./env` directly instead.

<Callout type="warn" title="env.ts is server-only">
	`env()` throws when it detects a browser runtime. The file holds your secrets.
	Browser code imports the generated client module instead.
</Callout>

## Boot fails, not the first request

`env()` validates the moment `env.ts` is evaluated. The generated index imports
that file before the runtime config. So a bad value throws before the app, the
adapters, auth and the database start.

Every failing variable lands in one error, listed by name. The values are never
printed. Validation is also synchronous. A schema that returns a promise throws
at once, naming the var.

## Where each name is read

The two sides look in different places for the same value.

| Declared as    | The server reads               | The browser reads                |
| -------------- | ------------------------------ | -------------------------------- |
| A `server` var | `DATABASE_URL`, that name only | Nothing. It is not in the bundle |
| A client var   | `APP_URL`, then `VITE_APP_URL` | `import.meta.env.VITE_APP_URL`   |

Unprefixed wins on the server. So a dev `.env` with `APP_URL` set is enough
there. The prefixed spelling is only a fallback.

The browser has no such fallback. It reads `VITE_APP_URL` only.

<Callout type="warn" title="Every build needs the prefixed name">
	A bundler inlines only prefixed vars. Set `VITE_APP_URL` wherever you build,
	dev included. Without it the var falls back to your schema default, or throws
	on import. The error names the prefixed var.
</Callout>

## The base variables

QUESTPIE merges twelve optional vars under your `server` block. So
`env.DATABASE_URL` is typed even when you never declared it, and so is
`env.NODE_ENV`. Re-declare one in `server` to tighten it. A plain `z.string()`
there makes it required.

They are optional because deployments differ. Cloud injects `QUESTPIE_DB`, a
self-host sets `DATABASE_URL`, and an explicit `db: { url }` in
`runtimeConfig()` needs neither. [Options](/docs/ship/environment/options) lists
all twelve.

## Neither file is required

A fresh app from `create-questpie` ships neither. Codegen emits nothing for env,
`app.env` stays an empty object, and nothing validates at boot.

QUESTPIE still reads `process.env` on its own for the fields you leave out of
`runtimeConfig()`. Each one has a Cloud name and a self-host fallback.

| Field    | Tried first        | Then                 |
| -------- | ------------------ | -------------------- |
| Database | `QUESTPIE_DB`      | `DATABASE_URL`       |
| App URL  | `QUESTPIE_APP_URL` | `APP_URL`            |
| Secret   | `QUESTPIE_SECRET`  | `BETTER_AUTH_SECRET` |

Add `env.ts` when you want that surface typed and the failure moved to boot.

## Where each topic lives

| Topic                                                  | Page                                              |
| ------------------------------------------------------ | ------------------------------------------------- |
| Every `env()` option, the base vars, the skip flag     | [Options](/docs/ship/environment/options)         |
| Consumers, prefixes, the generated browser module      | [Client variables](/docs/ship/environment/client) |
| Which file owns which setting, and what resolves alone | [Configuration](/docs/ship/configuration)         |
| What a deployment needs on top of these vars           | [Deploying](/docs/ship)                           |

## Next

**[Options](/docs/ship/environment/options)** covers `refine` for cross-field
rules. It also covers the flag that lets a build run with no environment.
