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.
| Prefix | Exposed to browser | Read with | Use for |
|---|---|---|---|
| BINI_ | Yes | import.meta.env | Public client config |
| VITE_ | Yes | import.meta.env | Public client config |
| No prefix | No | getEnv / requireEnv | Secrets - server only |
BINI_ and VITE_ prefixes are exposed to the browser by default. Variables without a prefix are never exposed to the client.bini-env v2 is fixed to ['BINI_', 'VITE_']. There is no option to add custom prefixes.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
BINI_PUBLIC_API_URL=https://api.example.com
BINI_APP_NAME=My App
BINI_ANALYTICS_ID=UA-XXXXexport 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>
<p>Analytics: {analyticsId}</p>
</div>
)
}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
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App
VITE_GA_ID=UA-XXXXXexport 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 (
<div>
<h1>{title}</h1>
<p>API: {apiUrl}</p>
<p>Analytics: {gaId}</p>
</div>
)
}VITE_* and BINI_* work 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
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
import { Hono } from 'hono'
import { 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,
smtpConfigured: !!smtpPass,
})
})
export default appBINI_* or VITE_* for sensitive data.Client-Side Access
Client-side variables are accessed via import.meta.env in any component:
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>
)
}The same works 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
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 Method | Where | Variables |
|---|---|---|
| import.meta.env | Client components | BINI_, VITE_ |
| getEnv(ctx, key) | API routes | All variables (including no prefix) |
| requireEnv(ctx, key) | API routes | All variables (throws if missing) |
getEnv vs requireEnv
Both getEnv and requireEnv read environment variables from the Hono request context, but they behave differently:
| Feature | getEnv(ctx, key) | requireEnv(ctx, key) |
|---|---|---|
| Returns | string | undefined | string |
| On missing | Returns undefined | Throws error immediately |
| Use case | Optional configuration with defaults | Required configuration |
| Default pattern | getEnv(ctx, 'KEY') ?? 'default' | requireEnv(ctx, 'KEY') |
| Error handling | Manual check for undefined | Try/catch or let it bubble |
| When to use | Feature flags, optional settings | Database URLs, API keys, credentials |
// src/app/api/compare.ts
import { Hono } from 'hono'
import { getEnv, requireEnv } from 'bini-env'
const app = new Hono()
app.get('/compare', (c) => {
const ctx = c as any
// 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')
return c.json({ debug, region, maxRetries, ready: !!(dbUrl && apiKey && smtpPass) })
})
export default apprequireEnv for critical configuration that your app cannot function without. Use getEnv with ?? defaults for optional configuration.On 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 - Do not 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.
.env.example
# .env.example - commit this file, never your real .env
# Public (exposed to the browser)
BINI_PUBLIC_API_URL=
VITE_APP_NAME=
# Secrets (server only)
DATABASE_URL=
JWT_SECRET=.gitignore
# .gitignore
.env
.env.local
.env.*.local