Varlock env schemas
Load a validated, typed .env schema with Varlock and read live config through the ENV proxy on every SSR render — with types, coercion, variable expansion, and redacted secrets.
Varlock turns your .env into a validated, typed schema — a .env.schema annotated with JSDoc-style decorators (@type, @required, @sensitive). Mochi renders every page on the server on each request, so there's nothing to prerender or statically inline: you load() the schema
once at boot, then read live values through the typed ENV proxy during SSR.
bun add varlockPoint package.json's varlock.loadPath at your schema, then do this once at the top of your server entry — index.ts, before Mochi.serve() — so config is validated before the server boots and every request can read it:
import { load } from 'varlock';
import { ENV } from 'varlock/env';
await load(); // parse + validate .env.schema once at boot
const port = ENV.DEMO_API_PORT; // typed + coerced: a number, not a stringThe index.ts tab below shows the full wiring, including patchGlobalConsole() to keep secrets out of your logs. See the decorator DSL at varlock.dev.
Resolved on this request
These values were parsed, coerced, and validated by varlock during this page's SSR render.
| Variable | Value | Reads as |
|---|---|---|
DEMO_APP_ENV | development | string |
DEMO_API_PORT | 8080 | number |
DEMO_API_URL | http://localhost:8080 | string |
DEMO_SECRET_KEY @sensitive | •••••••• | string |
DEMO_API_PORT arrives as a JavaScript number (8080), and DEMO_API_URL already has its ${DEMO_API_PORT} reference expanded to http://localhost:8080. The @sensitive key never leaves the server — it's masked before it reaches the props, and patchGlobalConsole() keeps it
out of your logs.
<script lang="ts">
import CodeSnippet from '../../components/CodeSnippet.svelte';
import Badge from '../../components/Badge.svelte';
import { highlightCode } from '../../lib/highlight.server';
import { files } from './files.ts';
import type { VarlockConfig } from './env.ts';
let { config }: { config: VarlockConfig } = $props();
const sources = await loadSources(files);
const codeInstall = await highlightCode('bun add varlock', 'bash');
const codeUsage = await highlightCode(
`import { load } from 'varlock';
import { ENV } from 'varlock/env';
await load(); // parse + validate .env.schema once at boot
const port = ENV.DEMO_API_PORT; // typed + coerced: a number, not a string`,
'typescript',
);
</script>
<div class="intro">
<p>
<a href="https://varlock.dev" target="_blank" rel="noopener noreferrer">Varlock</a> turns your
<code>.env</code> into a validated, typed schema — a <code>.env.schema</code> annotated with JSDoc-style decorators (<code>@type</code>, <code>@required</code>,
<code>@sensitive</code>). Mochi renders every page on the server on each request, so there's nothing to prerender or statically inline: you <code>load()</code> the schema
once at boot, then read live values through the typed <code>ENV</code> proxy during SSR.
</p>
<CodeSnippet html={codeInstall} />
<p>
Point <code>package.json</code>'s <code>varlock.loadPath</code> at your schema, then do this once at the top of your server entry — <code>index.ts</code>, before
<code>Mochi.serve()</code> — so config is validated before the server boots and every request can read it:
</p>
<CodeSnippet html={codeUsage} />
<p>
The <code>index.ts</code> tab below shows the full wiring, including <code>patchGlobalConsole()</code> to keep secrets out of your logs. See the decorator DSL at
<a href="https://varlock.dev" target="_blank" rel="noopener noreferrer">varlock.dev</a>.
</p>
</div>
<section class="card">
<header>
<h2>Resolved on this request</h2>
<p>These values were parsed, coerced, and validated by <code>varlock</code> during this page's SSR render.</p>
</header>
<table class="config">
<thead>
<tr><th>Variable</th><th>Value</th><th>Reads as</th></tr>
</thead>
<tbody>
{#each config.items as item (item.key)}
<tr>
<td class="var">
<code>{item.key}</code>
{#if item.isSensitive}<Badge kind="danger">@sensitive</Badge>{/if}
</td>
<td>
{#if item.isSensitive}
<span class="masked">••••••••</span>
{:else}
<code>{item.value}</code>
{/if}
</td>
<td><code>{item.jsType}</code></td>
</tr>
{/each}
</tbody>
</table>
<p class="note">
<code>DEMO_API_PORT</code> arrives as a JavaScript <code>number</code> ({config.apiPort}), and
<code>DEMO_API_URL</code> already has its <code>{'${DEMO_API_PORT}'}</code> reference expanded to
<code>{config.apiUrl}</code>. The <code>@sensitive</code> key never leaves the server — it's masked before it reaches the props, and <code>patchGlobalConsole()</code> keeps it
out of your logs.
</p>
</section>
Styles
<style>
.intro {
font-size: 0.95rem;
color: var(--text-muted);
margin: 0 0 1.75rem;
}
.intro p {
margin: 0 0 0.75rem;
}
.intro a {
color: var(--accent);
text-decoration: underline;
}
.intro > p:last-child {
margin-bottom: 0;
}
.intro p code {
background: var(--surface-muted);
border: 1px solid var(--border);
color: var(--text);
font-family: var(--font-mono);
padding: 0.1em 0.35em;
border-radius: 4px;
font-size: 0.85em;
}
.card {
margin-bottom: 2rem;
}
.card header {
margin-bottom: 0.9rem;
}
.card header p {
font-size: 0.95rem;
color: var(--text-muted);
margin: 0.25rem 0 0;
}
.card header code {
background: var(--surface-muted);
border: 1px solid var(--border);
color: var(--text);
font-family: var(--font-mono);
padding: 0.1em 0.35em;
border-radius: 4px;
font-size: 0.85em;
}
.config {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.config th {
text-align: left;
font-weight: 600;
color: var(--text-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.4rem 0.75rem;
border-bottom: 1px solid var(--border);
}
.config td {
padding: 0.55rem 0.75rem;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
.config td.var {
display: flex;
align-items: center;
gap: 0.5rem;
}
.config code {
font-family: var(--font-mono);
font-size: 0.85em;
color: var(--text);
}
.masked {
font-family: var(--font-mono);
letter-spacing: 0.15em;
color: var(--text-muted);
}
.note {
font-size: 0.9rem;
color: var(--text-muted);
margin: 1rem 0 0;
line-height: 1.6;
}
.note code {
background: var(--surface-muted);
border: 1px solid var(--border);
color: var(--text);
font-family: var(--font-mono);
padding: 0.1em 0.35em;
border-radius: 4px;
font-size: 0.85em;
}
</style>
More demos