bini-env
OfficialEnvironment 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
$ npm install bini-env hono
hono is a required peer dependency.Quick Start
1. Register the plugin
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
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 appNon-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.
| Prefix | Browser | Server-side | Use for |
|---|---|---|---|
| No prefix | Never | Yes | Secrets - API keys, DB URLs, tokens |
| BINI_ | Always | Yes | Public config |
| VITE_ | Always | Yes | Public config |
# 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-XXXXHow 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, thenroot, then the working directory. - Never overrides a value already set in
process.env(OS / shell / CI wins). - Skips empty values, so
requireEnvstill fails loudly on placeholders. - Respects
envDir: falseby skipping the mirror entirely.
On success it's silent. On failure it warns - but the dev server still boots:
10:45:55 (warning) [bini-env] Failed to inject .env into process.env: <reason>
The startup banner is unchanged:
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:
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
| Platform | Runtime | Source | How Hono reads it |
|---|---|---|---|
| Node.js | Node | System env / dev mirror | process.env |
| Bun | Bun | System env / dev mirror | process.env |
| Vercel Edge | V8 isolate | Project settings | process.env |
| Netlify Edge | Deno | Site settings | Deno.env.get() |
| Cloudflare Workers | V8 isolate | wrangler.toml / dashboard | c.env |
| Deno Deploy | Deno | Project settings | Deno.env.get() |
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.
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:
[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.
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.
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_andVITE_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, sorequireEnvthrows 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
| Tool | Version |
|---|---|
| Vite | 8.x |
| Hono | 4.x |
| TypeScript | 5.x |
| Node.js | ≥ 20.19 |