Prefixes & Client Exposure

Learn how environment variable prefixes work and which variables are exposed to the client.

What are Prefixes?

Environment variable prefixes determine which variables are exposed to the browser and which are kept server-side. The prefix tells Vite and Bini.js how to handle each variable.

Both BINI_ and VITE_ prefixes are exposed to the browser by default. Variables without a prefix are never exposed to the client.

BINI_ Prefix

BINI_ is the default prefix for client-side environment variables in Bini.js. These variables are exposed to the browser via import.meta.env.

.env
# .env
BINI_PUBLIC_API_URL=https://api.example.com
BINI_APP_NAME=My App
BINI_ANALYTICS_ID=UA-XXXX
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.BINI_APP_NAME
  const analyticsId = import.meta.env.BINI_ANALYTICS_ID
  
  return (
    <div>
      <h1>{appName}</h1>
      <p>API: {apiUrl}</p>
    </div>
  )
}
Important: BINI_* variables are bundled into your client-side JavaScript. Never put secrets in BINI_* variables.

VITE_ Prefix

VITE_ is Vite's standard prefix for client-side environment variables. Any variable starting with VITE_ is exposed to the browser.

.env
# .env
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App
VITE_GA_ID=UA-XXXXX
src/app/page.tsx
// src/app/page.tsx
export default function HomePage() {
  const apiUrl = import.meta.env.VITE_API_URL
  const title = import.meta.env.VITE_APP_TITLE
  const gaId = import.meta.env.VITE_GA_ID
  
  return <h1>{title}</h1>
}
Note: VITE_* and BINI_* work exactly the same way. Both are exposed to the browser. Choose whichever you prefer.

No Prefix (Secrets)

Variables without a prefix are never exposed to the browser. They are only accessible server-side via getEnv(ctx, key) in API routes.

.env
# .env
DATABASE_URL=postgres://localhost:5432/mydb
STRIPE_SECRET_KEY=sk_live_...
SMTP_PASS=super_secret
JWT_SECRET=your_jwt_secret
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
  
  // These are only accessible server-side
  const dbUrl = requireEnv(ctx, 'DATABASE_URL')
  const jwtSecret = requireEnv(ctx, 'JWT_SECRET')
  const smtpPass = requireEnv(ctx, 'SMTP_PASS')
  
  // Never expose secrets in responses
  return c.json({ 
    dbConnected: !!dbUrl,
    jwtConfigured: !!jwtSecret 
  })
})

export default app
Critical: Variables without a prefix are the only way to keep secrets secure. Never use BINI_* or VITE_* for sensitive data.

Custom Prefixes

You can add custom prefixes to expose additional variables to the client:

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

export default defineConfig({
  plugins: [
    biniEnv({
      envPrefix: ['PUBLIC_', 'MY_APP_']
    })
  ]
})
.env
# .env
PUBLIC_API_URL=https://api.example.com
PUBLIC_APP_NAME=My App
MY_APP_VERSION=1.0.0
BINI_ANALYTICS_ID=UA-XXXX
// All of these are accessible in the browser
import.meta.env.PUBLIC_API_URL
import.meta.env.PUBLIC_APP_NAME
import.meta.env.MY_APP_VERSION
import.meta.env.BINI_ANALYTICS_ID
PrefixExposed to browser
BINI_Yes (default)
VITE_Yes (default)
PUBLIC_Yes (custom)
MY_APP_Yes (custom)
No prefixNo
Adding custom prefixes is useful when you want to use a different naming convention for your public environment variables.

Client-Side Access

Client-side variables are accessed via import.meta.env in any component:

// src/app/page.tsx
export default function Page() {
  // Access client-side variables
  const apiUrl = import.meta.env.BINI_API_URL
  const appName = import.meta.env.VITE_APP_NAME
  
  return (
    <div>
      <h1>{appName}</h1>
      <p>API: {apiUrl}</p>
    </div>
  )
}

// In MDX files
export const metadata = {
  title: import.meta.env.VITE_APP_NAME,
}

# Welcome to {import.meta.env.VITE_APP_NAME}
import.meta.env is available in all client-side code including pages, components, and MDX files.

Server-Side Access

Server-side variables are accessed via getEnv(ctx, key) and requireEnv(ctx, key) in API routes:

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

const app = new Hono()

app.post('/email/send', async (c) => {
  const ctx = c as any
  
  // Server-side secrets (no prefix)
  const smtpHost = requireEnv(ctx, 'SMTP_HOST')
  const smtpPass = requireEnv(ctx, 'SMTP_PASS')
  const fromEmail = requireEnv(ctx, 'FROM_EMAIL')
  
  // Optional config with defaults
  const smtpPort = parseInt(getEnv(ctx, 'SMTP_PORT') ?? '587')
  
  // Client-side config (BINI_)
  const publicUrl = getEnv(ctx, 'BINI_API_URL')
  
  return c.json({ 
    success: true,
    publicUrl, // This is safe to return
    // smtpPass is NEVER returned to the client
  })
})

export default app
Access MethodWhereVariables
import.meta.envClient componentsBINI_, VITE_, custom prefixes
getEnv(ctx, key)API routesAll variables (including no prefix)
requireEnv(ctx, key)API routesAll variables (throws if missing)

getEnv vs requireEnv

Both getEnv and requireEnv read environment variables from the Hono request context, but they behave differently:

FeaturegetEnv(ctx, key)requireEnv(ctx, key)
Returnsstring | undefinedstring
On missingReturns undefinedThrows error immediately
Use caseOptional configuration with defaultsRequired configuration
Default patterngetEnv(ctx, 'KEY') ?? 'default'requireEnv(ctx, 'KEY')
Error handlingManual check for undefinedTry/catch or let it bubble
When to useFeature flags, optional settingsDatabase URLs, API keys, credentials
// getEnv — for optional values
const debug = getEnv(ctx, 'DEBUG_MODE') === 'true'
const region = getEnv(ctx, 'AWS_REGION') ?? 'us-east-1'
const maxRetries = parseInt(getEnv(ctx, 'MAX_RETRIES') ?? '3')

// requireEnv — for required values
const dbUrl = requireEnv(ctx, 'DATABASE_URL')
const apiKey = requireEnv(ctx, 'STRIPE_SECRET_KEY')
const smtpPass = requireEnv(ctx, 'SMTP_PASS')
Best practice: Use requireEnv for critical configuration that your app cannot function without. Use getEnv with ?? defaults for optional configuration.

On terminal failure: requireEnv logs a descriptive error to the terminal:

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

Security Best Practices

  • Never prefix secrets — Use no prefix for database URLs, API keys, and tokens.
  • Use BINI_ or VITE_ for public config — Use these for non-sensitive configuration like API URLs.
  • Use requireEnv for critical values — Fail fast when required configuration is missing.
  • Use getEnv with defaults for optional values — Keep your app flexible with sensible defaults.
  • Never expose secrets in responses — Don't return secret values from API routes.
  • Use .env.example — Document required variables without committing actual values.
  • Keep .env in .gitignore — Never commit environment files with secrets.