The schema
createAppEnv carries the Fusion stack's canonical set of variables. The schema
is defined once, in one file, and embedded here straight from the source — so
this page can't drift from the code:
import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";
export function createAppEnv(runtimeEnv: Record<string, string | undefined>) {
return createEnv({
server: {
SERVER_URL: z.string().url().optional(),
DATABASE_URL: z.string().url().optional(),
// Optional: required when the in-app Carola chat is exercised. The
// existing demo routes also read process.env.ANTHROPIC_API_KEY
// directly, so leaving this optional lets the rest of the app boot
// without it.
ANTHROPIC_API_KEY: z.string().min(1).optional(),
// Powers the dashboard Carola chat — `/api/ai/chat` uses the
// OpenRouter adapter so the model is swappable via this one key.
OPENROUTER_API_KEY: z.string().min(1).optional(),
// Connects the web app + worker to the self-hosted Hatchet engine
// (background jobs). Optional: when unset, uploads are NOT enqueued and
// the app boots normally — same graceful-degradation contract as the AI
// keys above. The worker process requires it. Generate it from the
// Hatchet dashboard (Settings → API Tokens) or the admin CLI.
HATCHET_CLIENT_TOKEN: z.string().min(1).optional(),
},
clientPrefix: "VITE_",
client: {
VITE_APP_TITLE: z.string().min(1).optional(),
},
runtimeEnv,
emptyStringAsUndefined: true,
});
}
export type AppEnv = ReturnType<typeof createAppEnv>;The variables
Every variable is optional — fusion-env validates shape, not presence, so
an app boots even when an integration is unconfigured (it just degrades that one
feature). Provide a value and it must be valid, or createAppEnv throws.
| Variable | Scope | Rule | Purpose |
|---|---|---|---|
SERVER_URL | server | URL | The app's own base URL. |
DATABASE_URL | server | URL | Database connection string. |
ANTHROPIC_API_KEY | server | non-empty | Anthropic key for the in-app Carola chat. Unset → that feature is off. |
OPENROUTER_API_KEY | server | non-empty | Powers the dashboard Carola chat (/api/ai/chat, swappable model). |
HATCHET_CLIENT_TOKEN | server | non-empty | Connects the app + worker to Hatchet (background jobs). The worker requires it. |
VITE_APP_TITLE | client | non-empty | App title — safe to expose to the browser. |
Server and client are kept apart
@t3-oss/env-core splits the schema by a clientPrefix of VITE_. Only
VITE_-prefixed vars are allowed to reach a browser bundle; everything else is
server-only, so a secret like DATABASE_URL can never be read from client code.
emptyStringAsUndefined: true rounds it out — a blank SERVER_URL= is read as
"not set" rather than an empty, invalid value.
Types come from the schema
The return type is inferred from Zod, so there is no hand-written interface to keep in sync. Hover the inferred type:
import { } from "zod";
// A slice of the real schema — the env type is inferred straight from it.
const = .({
: .().().(),
: .().().(),
: .().(1).(),
});
type = .<typeof >;
fusion-env exports exactly this as AppEnv (ReturnType<typeof createAppEnv>),
so your app code stays typed end to end.