Skip to content
fusion-env

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:

index.ts
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.

VariableScopeRulePurpose
SERVER_URLserverURLThe app's own base URL.
DATABASE_URLserverURLDatabase connection string.
ANTHROPIC_API_KEYservernon-emptyAnthropic key for the in-app Carola chat. Unset → that feature is off.
OPENROUTER_API_KEYservernon-emptyPowers the dashboard Carola chat (/api/ai/chat, swappable model).
HATCHET_CLIENT_TOKENservernon-emptyConnects the app + worker to Hatchet (background jobs). The worker requires it.
VITE_APP_TITLEclientnon-emptyApp 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.

Loading diagram...

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.