Environment Variables

Zero-config environment variable system powered by Hono — works across Node.js, Bun, Deno, Vercel Edge, Netlify Edge, and Cloudflare Workers.

bini-env is installed and configured by default in every Bini.js project. It reads env vars from the Hono request context, so variables are always resolved from the correct runtime binding — no platform-specific code needed.

Hono-native: getEnv(c, key) / requireEnv(c, key) read directly from the Hono request context. Zero dotenv — no .env parsing at runtime; vars come from the host platform. Vite handles .env loading during development.

Quick Start

1. Register the Vite plugin

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

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

2. Read env vars in your Hono handlers

src/app/api/hello.ts
// 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

    const apiKey  = requireEnv(ctx, 'MY_API_KEY')
    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

Usage Pattern

Always pass c explicitly. Cast it once at the top of the handler, then use ctx throughout.

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

    const dbUrl  = requireEnv(ctx, 'DATABASE_URL')
    const apiKey = requireEnv(ctx, 'STRIPE_SECRET_KEY')

    const model      = getEnv(ctx, 'AI_MODEL')    ?? 'gpt-4o'
    const debug      = getEnv(ctx, 'DEBUG_MODE')  === 'true'

    // ... rest of handler

  } 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)
  }
})

The pattern in three steps:

const ctx = c as any              // cast once
requireEnv(ctx, 'KEY')            // throws if missing
getEnv(ctx, 'KEY') ?? 'default'   // optional with default

Environment Prefixes

BINI_ — Client-side vars

BINI_ variables are exposed to import.meta.env. Use them for public client-side config.

.env
# .env
BINI_PUBLIC_API_URL=https://api.example.com
const apiUrl = import.meta.env.BINI_PUBLIC_API_URL

VITE_ — Public client vars

VITE_ is Vite's built-in prefix. Any var starting with VITE_ is bundled into your client-side JavaScript.

.env
# .env
VITE_ANALYTICS_ID=UA-XXXX
import.meta.env.VITE_ANALYTICS_ID

No prefix — Secrets (server only)

Variables without a prefix are NOT exposed to the browser. Read them via getEnv(ctx, key) in API routes only.

.env
# .env
DATABASE_URL=postgres://...
STRIPE_SECRET_KEY=sk_live_...
const ctx = c as any
const dbUrl = requireEnv(ctx, 'DATABASE_URL')

Prefix Summary

PrefixExposed to browserUse for
BINI_YesPublic client config
VITE_YesPublic client config
No prefixNoSecrets — server only
Critical: Never put secrets in BINI_* or VITE_* variables — both are exposed to the browser. Use un-prefixed variables for secrets and read them with getEnv(ctx, key) inside API route handlers only.

Platform Support

getEnv and requireEnv delegate to Hono's env(c) adapter, which reads from the correct source on every supported platform automatically.

PlatformRuntimeHow Hono reads it
Node.jsNodeprocess.env
BunBunprocess.env
Vercel EdgeV8 isolateprocess.env
Netlify EdgeDenoDeno.env.get()
Cloudflare WorkersV8 isolateCF bindings via c.env
Deno DeployDenoDeno.env.get()

How It Works

The biniEnv() plugin tells Vite which env prefixes to expose to import.meta.env:

config() {
  return { envPrefix: ['BINI_', 'VITE_', ...yourExtras] }
}

On server start you will see:

  ß Bini.js (dev)
  ➜  Environments: .env.local, .env
  ➜  Local:   http://localhost:3000/

Vite handles everything natively: loading .env files, watching, restarting, injecting prefixed vars, and HMR. bini-env does not reimplement any of that.

Zero dotenv: dotenv is never used at runtime. In production, vars are set in your hosting platform's environment config.

API Reference

getEnv(c, key)

Returns string | undefined.

app.get('/config', async (c) => {
  const ctx = c as any
  const region = getEnv(ctx, 'AWS_REGION') ?? 'us-east-1'
  return c.json({ region })
})

requireEnv(c, key)

Returns string. Throws if missing.

app.post('/send-email', async (c) => {
  const ctx = c as any
  const smtpHost = requireEnv(ctx, 'SMTP_HOST')
  // ...
})

On failure, the terminal will show:

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

biniEnv(options?)

biniEnv()
biniEnv({ envPrefix: ['MY_PUBLIC_'] })
OptionTypeDefaultDescription
envPrefixstring | string[][]Extra prefixes to expose

Performance

MetricDevProd
File reads00
Runtime cost~0ms0
Bundle impactMinimalTree-shaken

No dotenv. No disk reads. No caching layer.

Troubleshooting

ProblemSolution
Env var undefined in productionSet variables in your hosting platform's environment dashboard.
Works in dev, undefined in prodProduction requires platform-level configuration.
Cloudflare secret not foundSecrets set via wrangler secret put are only available via c.env. Ensure you are passing c to the function.
TypeScript error: Context not assignableCast once per handler: const ctx = c as any

Complete Example

.env
# .env
BINI_PUBLIC_API_URL=https://api.example.com
VITE_APP_NAME=My App
DATABASE_URL=postgres://localhost:5432/mydb
JWT_SECRET=your_jwt_secret
src/app/page.tsx
// src/app/page.tsx
export default function HomePage() {
  const apiUrl = import.meta.env.BINI_PUBLIC_API_URL
  const appName = import.meta.env.VITE_APP_NAME
  return <h1>{appName}</h1>
}
src/app/api/config.ts
// src/app/api/config.ts
import { Hono } from 'hono'
import { getEnv, requireEnv } from 'bini-env'

const app = new Hono()

app.get('/config', (c) => {
  const ctx = c as any
  const dbUrl = requireEnv(ctx, 'DATABASE_URL')
  const jwtSecret = requireEnv(ctx, 'JWT_SECRET')
  const debug = getEnv(ctx, 'DEBUG_MODE') === 'true'

  return c.json({ debug, dbConnected: !!dbUrl })
})

export default app