Environment variables
Which values reach the browser is declared, not spelled. An app lists the public ones in
fluixi.config.ts; everything else stays on the server, whatever it is called.
// fluixi.config.ts
import { defineConfig } from '@fluixi/start/config';
export default defineConfig({
env: {
public: ['AUTH_ISSUER', 'AUTH_CLIENT_ID', 'AUTH_TOKENS', 'API_URL', 'APP_URL'],
},
});
Vite's convention puts the decision in the name: a VITE_ prefix publishes the value. Moving
a value between server and browser then renames every reference, and a secret named
VITE_API_SECRET ships whether anything reads it or not. A list means a secret can only reach
the bundle by being written into it.
Reading them
import { publicEnv, serverEnv } from '@fluixi/start/env';
const { API_URL } = publicEnv(); // anywhere
const secret = serverEnv('SESSION_SECRET', { required: true }); // server only
publicEnv() works on both sides and returns the same frozen set on each. In the browser the
build has defined it as a literal. On the server it reads process.env, narrowed to the same
declared names, so a loader cannot use a value the browser will not have. It also works when
the server imports @fluixi/start from node_modules instead of bundling it, which is the
default for a published install: there import.meta.env is undefined, and code reading it
breaks in production only.
serverEnv(name) reads one server-only variable. In a browser it throws instead of returning
undefined, because a secret read as undefined becomes a request sent without credentials,
and the failure then points at the request instead of at the read. { required: true } also
throws when the variable is unset, naming it.
Where the values come from
fluixi dev and fluixi start load .env, .env.local, .env.<mode> and
.env.<mode>.local into process.env on the server, every variable, prefixed or not. A
variable already set in the real environment wins over the files, which is how a deployment's
own secrets override a developer's .env.
The VITE_ prefix
It keeps working. A declared name is found under its bare spelling or with VITE_ in front,
bare first, so .env can move one variable at a time: AUTH_ISSUER in env.public reads
VITE_AUTH_ISSUER until it is renamed. Anything already prefixed stays public, as it always
was.
What stops a secret shipping
The list is the protection. Three checks catch the line written in a hurry:
| Check | When | What happens |
|---|---|---|
a name in env.public reads like a secret (SECRET, PASSWORD, PRIVATE_KEY, CREDENTIALS) |
build | refused, naming it |
a VITE_ variable in any .env file reads like a secret |
fluixi build |
refused; fluixi dev warns |
serverEnv() called in the browser |
run time | throws |
For a name that only reads like a secret, FLUIXI_ALLOW_PUBLIC_SECRETS=true lets the build
through.
Next: SEO and the document head.