bini-env

Official

Environment variable system + Vite plugin for Bini.js. Hono-native, universal runtime, zero-config dev secrets.

Overview

bini-env reads environment variables from the Hono request context, so they always resolve from the correct runtime binding - Node.js, Bun, Deno, Vercel Edge, Netlify Edge, or Cloudflare Workers - without any per-platform code.

In dev, it also mirrors non-prefixed .env values into process.env so Node-hosted Hono routes can read server-side secrets with no manual setup.

Installation

>_Terminal
$ npm install bini-env hono
hono is a required peer dependency.

Quick Start

1. Register the plugin

vite.config.ts
import { defineConfig } from 'vite'
import { biniEnv } from 'bini-env'

export default defineConfig({
  plugins: [biniEnv()],
})

biniEnv() takes no options.

2. Read env vars in a Hono handler

src/app/api/hello.ts
import { Hono } from 'hono'
import { getEnv, requireEnv } from 'bini-env'

const app = new Hono()

app.post('/hello', async (c) => {
  try {
    const ctx = c as any

    // Throws if missing — use for required config
    const apiKey = requireEnv(ctx, 'MY_API_KEY')

    // Returns undefined if missing — use for optional config
    const appName = getEnv(ctx, 'APP_NAME') ?? 'World'

    return c.json({ message: `Hello, ${appName}!` })
  } catch (error: any) {
    if (error.message?.includes('[bini-env] Missing required')) {
      return c.json({ error: error.message }, 500)
    }
    return c.json({ error: 'Something went wrong.' }, 500)
  }
})

export default app

Non-prefixed keys like MY_API_KEY are read from your .env file in dev. In production, set them in your hosting platform's environment config.

Prefixes

Two prefixes are exposed to the browser: BINI_ and VITE_. Everything else stays server-side.

PrefixBrowserServer-sideUse for
No prefixNeverYesSecrets - API keys, DB URLs, tokens
BINI_AlwaysYesPublic config
VITE_AlwaysYesPublic config
.env
# Server-side only
DATABASE_URL=postgres://...
STRIPE_SECRET_KEY=sk_live_...

# Exposed to the browser
BINI_API_URL=https://api.example.com
VITE_ANALYTICS_ID=UA-XXXX
The prefix list is fixed - there's no config surface that can widen what's exposed. Use no prefix for anything secret.

How It Works

Dev and preview

On vite dev / vite preview, biniEnv() loads your .env* files with Vite's own loadEnv and mirrors every non-prefixed value into process.env. That's what makes server-side secrets readable inside Hono routes without a manual loop.

  • Runs only in dev / preview - never on vite build.
  • Reads from Vite's envDir, then root, then the working directory.
  • Never overrides a value already set in process.env (OS / shell / CI wins).
  • Skips empty values, so requireEnv still fails loudly on placeholders.
  • Respects envDir: false by skipping the mirror entirely.

On success it's silent. On failure it warns - but the dev server still boots:

>_Terminal
10:45:55 (warning) [bini-env] Failed to inject .env into process.env: <reason>

The startup banner is unchanged:

>_Terminal
  Bini.js (dev)
  Environments: .env.local, .env
  Local:   http://localhost:3000/
  Network: http://192.168.1.7:3000/

If you already have a manual loadEnv loop in vite.config.ts, delete it - this plugin replaces it:

vite.config.ts
export default defineConfig({
  plugins: [biniEnv()],
})

Reading env vars

getEnv and requireEnv read from env(c) via hono/adapter. Every read is request-scoped and resolved by Hono for the current platform. dotenv is never used.

Platform Support

PlatformRuntimeSourceHow Hono reads it
Node.jsNodeSystem env / dev mirrorprocess.env
BunBunSystem env / dev mirrorprocess.env
Vercel EdgeV8 isolateProject settingsprocess.env
Netlify EdgeDenoSite settingsDeno.env.get()
Cloudflare WorkersV8 isolatewrangler.toml / dashboardc.env
Deno DeployDenoProject settingsDeno.env.get()
Cloudflare: secrets set via wrangler secret put are only available via c.env inside a handler. Pass c to getEnv / requireEnv and they resolve correctly.

API Reference

getEnv(c, key)

Returns string | undefined.

src/app/api/config.ts
const region = getEnv(ctx, 'AWS_REGION') ?? 'us-east-1'
const debug  = getEnv(ctx, 'DEBUG_MODE') === 'true'

requireEnv(c, key)

Returns string. Throws if the variable is missing or empty, and logs the failure to the terminal:

>_Terminal
[bini-env] error  Missing required environment variable: "SMTP_HOST"
  -> Set it in your platform's env config or hosting dashboard.

biniEnv()

Vite plugin. Takes no options.

biniLogger

Vite-style logger for your own plugins or server code.

src/lib/logger.ts
import { biniLogger } from 'bini-env'

biniLogger.info('Server ready')
biniLogger.warn('Missing optional var')
biniLogger.error('Something broke', error)

HonoContext

Exported type (Context from Hono). Use it to type helpers that group env reads.

src/lib/db-config.ts
import type { HonoContext } from 'bini-env'

function readDbConfig(c: HonoContext) {
  const ctx = c as any
  return {
    url: requireEnv(ctx, 'DATABASE_URL'),
    poolSize: parseInt(getEnv(ctx, 'DB_POOL_SIZE') ?? '10'),
  }
}

Security

  • Never prefix secrets. BINI_ and VITE_ are always exposed to the browser.
  • Use no prefix for secrets. Available server-side via getEnv / requireEnv.
  • Don't leave secret placeholders empty. API_KEY= is skipped, so requireEnv throws instead of silently returning an empty string.
  • Let OS / CI win in dev. Override locally with a shell var instead of editing .env:
    >_Terminal
    $ DATABASE_URL=postgres://staging... pnpm dev

Compatibility

ToolVersion
Vite8.x
Hono4.x
TypeScript5.x
Node.js≥ 20.19
Was this helpful?