brumate

Environment variables

Secrets and settings come from the environment: a database URL, a session secret, the name of the site. brumate checks them with a schema when the server starts, types them, and keeps the server's values out of the browser.

Declaring them

One file declares every variable, split into the ones that only the server reads and the ones that the browser reads too:

// src/env.ts
import { defineEnv } from 'brumate/env';
import * as v from 'valibot';

export default defineEnv({
  server: {
    DATABASE_URL: v.pipe(v.string(), v.url()),
    SESSION_SECRET: v.pipe(v.string(), v.minLength(32)),
  },
  public: {
    SITE_NAME: v.optional(v.string(), 'Tracker'),
  },
});

On the server

The server checks every variable when the file loads, which the plugin makes the first thing the server loads. A server that starts without a secret, or with a wrong one, fails at once, and the error names every variable that is missing or wrong:

Error: Environment variables: DATABASE_URL is missing. SESSION_SECRET is invalid

For a server variable, the error gives the name and the path of each issue, not its message, because the messages of some schema libraries hold the value. A public variable's error gives the messages too.

Components, server functions, middleware and endpoints import the file and read the values with their types:

import env from './env.ts';

async function listIssues() {
  'use server';
  return db.connect(env.DATABASE_URL).issues.list();
}

In the browser

The browser gets the file without the server variables: the plugin removes their schemas, and writes in the values of the public variables, read when the dev server or the build ran. The public schemas check them in the browser as the server's do on the server, so a default such as 'Tracker' works in both.

<script>
  import env from './env.ts';
</script>

<h1>{env.SITE_NAME}</h1>

Code that the browser loads imports the file only from components. A script that the browser loads, such as a helper that a handler calls, cannot import it, by any path: the build fails and names the script. The compiler checks what a component reads, and it cannot check a script. Read the variable in a component or a server function, and pass the value on.

The compiler knows which code runs in the browser, so it reports a read of a server variable there, in a handler, an effect, a $client callback or a memo that the browser computes again. A value made from a server variable that the browser reads, such as a signal, is reported too, because the page state would send it:

<script>
  import { $signal } from 'brumate/signals';
  import env from './env.ts';

  let url = $signal(env.DATABASE_URL);
  // "url" holds env.DATABASE_URL, a server variable, and code that runs in the browser reads it
</script>

<button onClick=(() => console.log(env.DATABASE_URL))>Show</button>
<!-- env.DATABASE_URL, a server variable, is read by code that runs in the browser -->

The plugin finds the imports of the env file by the path that they resolve to, so an alias such as ~/env counts too.

A server variable that a component reads while it renders, such as in a memo that the server computes and the browser never does, stays on the server. The check is on the names of the variables, which a prefix in the name cannot do: SITE_NAME is public because the file says so, not because it starts with VITE_.

In the page state

The compiler sees the reads in a component only. A helper in another module can read a secret, and a component can put what it returns in a signal that the browser reads:

<script>
  import { $signal } from 'brumate/signals';
  import { connectionString } from './db.ts';

  let url = $signal(connectionString());
</script>

<button onClick=(() => console.log(url))>Show</button>

The server checks the page state too. defineEnv keeps the values of the server variables when the server starts, and the server refuses to send a page whose state holds one of them as a string. The render fails, and the error names the slot and its component, as it does for a value that cannot be sent:

The value of "url" in Page_1x2y3z is the value of a server environment variable, and code that runs in the browser reads it.

Reference

Export What it does
defineEnv(schema, options?) Checks the variables of schema.server and schema.public and returns their values, typed. Throws for a wrong one.
options.source Where the values come from. Defaults to process.env. The browser's copy of the file passes the public values.
Env<S>, EnvSchema, EnvOptions The types of the values, of the schema and of the options.