Skip to content

Elysia

Elysia needs no integration package. Import varlock/auto-load and read values through the typed ENV proxy, the same as any other JavaScript project.

The part worth reading is deploying. Elysia has no build step that inlines env values, so unlike Next.js or Vite there is nothing to bake your config into. varlock freeze fills that gap.

Check out the Elysia example project for a working reference.


  1. Install varlock

    Terminal window
    npm install varlock
  2. Run varlock init to set up your .env.schema

    Terminal window
    npm exec -- varlock init
  3. Turn off Bun’s own .env loading

    Bun loads .env files itself based on NODE_ENV/BUN_ENV, which feeds values into varlock behind its back. Let varlock own env loading:

    bunfig.toml
    env = false

    See the Bun integration docs for the other ways to disable it, including for compiled binaries.

  4. Import varlock/auto-load first in your entrypoint

    src/server.ts
    import 'varlock/auto-load';
    import { ENV } from 'varlock/env';
    import { Elysia } from 'elysia';
    new Elysia()
    .get('/', () => ({ message: ENV.PUBLIC_MESSAGE }))
    .listen(ENV.PORT);

    The import must come first, so env is loaded and validated before anything else runs. If you would rather keep it out of app code, drop the import and launch with varlock run -- bun src/server.ts instead.

Because varlock knows which items are @sensitive, both protections work in Elysia with no extra setup:

.get('/log-demo', () => {
console.log('SOME_API_KEY =', ENV.SOME_API_KEY); // logged redacted
return 'ok';
})
.get('/leak-demo', () => {
return { oops: ENV.SOME_API_KEY }; // blocked, request fails with a 500
})

Elysia builds responses with the standard Response API, which varlock patches, so a sensitive value returned from a handler is caught before it reaches the client. See leak prevention.

Locally, varlock resolves your .env files on every boot. In a deploy that is usually the wrong default: it means config lives in your platform’s settings rather than in the release, so it can’t change atomically with your code and doesn’t roll back with it. It also makes every boot depend on your secret backend being reachable.

varlock freeze resolves everything once at deploy time and writes an encrypted file that ships inside your deploy artifact. Your app boots from that file.

  1. Generate a key, once

    Terminal window
    npm exec -- varlock generate-key

    Set the result as _VARLOCK_ENV_KEY in your deploy pipeline and your runtime environment, and set _VARLOCK_USE_FROZEN_ENV=1 in the runtime environment next to it so the app boots from the frozen file.

  2. Bundle and freeze at deploy time

    Terminal window
    bun build ./src/server.ts --target=bun --outdir dist
    APP_ENV=production varlock freeze

    Run freeze where your .env files and secret backends are reachable, usually a CI job. It writes .varlock-frozen-env and prints which environment it captured.

  3. Ship the frozen file with your code

    Dockerfile
    FROM oven/bun:1
    WORKDIR /app
    COPY dist/server.js bunfig.toml ./
    COPY .varlock-frozen-env ./
    ENV _VARLOCK_USE_FROZEN_ENV=1
    CMD ["bun", "server.js"]

    Copy it into the image rather than mounting it at runtime. Mounting it separately puts config back outside the release, which is the thing this avoids.

  4. Boot normally

    Terminal window
    bun server.js

That image needs nothing else: no .env files, no node_modules, and no varlock CLI. Values, coerced types, and sensitivity all travel inside the frozen file, so ENV.PORT is still a number and log redaction still works.

If your platform assigns PORT to each instance, mark it @dynamic=boot in your schema. Otherwise the frozen value wins over the one the platform sets.

The file is only read when _VARLOCK_USE_FROZEN_ENV is set, so a missing frozen file is a hard error instead of a silent fall back to normal resolution, and a leftover file in your dev checkout changes nothing.

bun build --compile works the same way. With _VARLOCK_USE_FROZEN_ENV=1 the binary reads .varlock-frozen-env from its working directory, so ship the two together:

Terminal window
bun build ./src/server.ts --compile --outfile server --no-compile-autoload-dotenv
APP_ENV=production varlock freeze

--no-compile-autoload-dotenv stops the compiled binary from doing Bun’s own .env loading, matching the bunfig.toml setting above.

import 'varlock/auto-load' is what the binary needs, and it bundles cleanly. It never shells out to the varlock CLI when it has a frozen file or a trusted blob to boot from, so a distroless image with no shell and no node_modules works. Keep the import first in your entrypoint.

If you cannot get a file into the image (a Compose file that pulls a tag it does not rebuild, for instance), varlock freeze --out - writes the same payload to stdout and you carry it in one variable:

Terminal window
export __VARLOCK_ENV=$(APP_ENV=production varlock freeze --out -)
compose.yaml
services:
app:
environment:
__VARLOCK_ENV: ${__VARLOCK_ENV:?run `varlock freeze --out -` first}
_VARLOCK_USE_INJECTED_ENV: "1"
_VARLOCK_ENV_KEY: ${_VARLOCK_ENV_KEY:?}

_VARLOCK_USE_INJECTED_ENV=1 is required: without it varlock tries to verify the payload against .env files the image does not have, and falls back to running the CLI. See carrying the payload in an env var for what this trades away versus shipping a file.

The environment comes from whatever your schema’s @currentEnv reads, so set that when you freeze. The summary names the environment it captured, since freezing the wrong one is the easiest mistake to make and the hardest to notice:

Terminal window
APP_ENV=production varlock freeze

freeze is not the only way to deploy. If the varlock CLI, your .env files, and your resolver credentials are all available in the runtime environment, varlock run -- bun server.js works too and re-resolves on every boot. See related approaches for the comparison.