Environment variables
This boilerplate uses environment variables for configuration. All sensitive credentials and environment-specific settings should be stored in .env files.
Quick start
- Copy the example file:
cp .env.example .env - Configure required core variables (see below)
- Add optional integrations as needed
- Restart your dev server
.env files to version control. The .gitignore file already excludes them.Variables reference
| Variable | Required | Scope | Description |
|---|---|---|---|
NUXT_PUBLIC_SITE_URL | ✅ | Public | Your site's public URL |
NUXT_PUBLIC_SITE_NAME | ✅ | Public | Site name (emails, meta tags) |
NUXT_PUBLIC_SITE_DOMAIN | ✅ | Public | Domain name (email sender) |
NUXT_PUBLIC_BLOG_SOURCE | ⚪ | Public | Blog source mode: md (Nuxt Content) or db (blog posts API/database) |
DATABASE_URL | ✅ | Server | PostgreSQL connection string |
DATABASE_POOL_MAX | ⚪ | Server | pg.Pool max connections (default 10) |
DATABASE_POOL_TIMEOUT_MS | ⚪ | Server | pg.Pool connection timeout in ms (default 20000) |
RESEND_API_KEY | ⚪ | Server | Resend API key for email delivery (OTPs log to console when unset) |
BETTER_AUTH_SECRET | ✅ | Server | Secret for signing auth tokens |
BETTER_AUTH_URL | ✅ | Server | Base URL for authentication |
BETTER_AUTH_TRUSTED_ORIGINS | ⚪ | Server | Extra CORS / better-auth trusted origins (comma-separated) |
BETTER_AUTH_TRUSTED_PROXIES | ⚪ | Server | IPs/CIDRs of proxies in front of the app (comma-separated) — see below |
REVIEW_ACCOUNT_EMAILS | ⚪ | Server | App store reviewer emails that sign in with a static code (comma-separated) — see below |
REVIEW_ACCOUNT_OTP | ⚪ | Server | The static sign-in code for REVIEW_ACCOUNT_EMAILS — see below |
CONTACT_FORM_TO_EMAILS | ⚪ | Server | Contact form recipient email(s) (comma-separated for multiple) |
NUXT_PUBLIC_DISPLAY_CONTACT_EMAIL | ⚪ | Public | Public email address displayed in the contact form |
STRIPE_SECRET_KEY | ⚪ | Server | Stripe secret key |
NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | ⚪ | Public | Stripe publishable key |
NUXT_PUBLIC_STRIPE_LIVE_MODE | ⚪ | Public | Build-time flag: true selects live Stripe price IDs from payments.config.ts |
STRIPE_WEBHOOK_SECRET | ⚪ | Server | Stripe webhook signing secret |
OPENAI_API_KEY | ⚪ | Server | OpenAI API key |
ANTHROPIC_API_KEY | ⚪ | Server | Anthropic API key |
GROK_API_KEY | ⚪ | Server | Grok / xAI API key |
NUXT_GEO_PROVIDER | ⚪ | Server | Country detection provider: ipinfo (default) or cloudflare |
IPINFO_TOKEN | ⚪ | Server | IPinfo geolocation token |
NUXT_PUBLIC_UMAMI_ID | ⚪ | Public | Umami website ID |
NUXT_PUBLIC_UMAMI_HOST | ⚪ | Public | Umami host URL |
NUXT_PUBLIC_VERCEL_ANALYTICS | ⚪ | Public | Enable Vercel Analytics |
Required variables
Site configuration
# Your site's public URL
NUXT_PUBLIC_SITE_URL="http://localhost:3000"
# Site name (used in emails, meta tags, etc.)
NUXT_PUBLIC_SITE_NAME="Your App Name"
# Domain name (used in email sender addresses)
NUXT_PUBLIC_SITE_DOMAIN="yourdomain.com"
Database
# PostgreSQL connection string
DATABASE_URL="postgresql://user:password@localhost:5432/database?sslmode=require"
# Optional pg.Pool overrides (configured in lib/prisma.ts — not URL query params)
# DATABASE_POOL_MAX=10
# DATABASE_POOL_TIMEOUT_MS=20000
Format: postgresql://[user]:[password]@[host]:[port]/[database]
Pool sizing is set on the pg.Pool in lib/prisma.ts (max / connectionTimeoutMillis), with optional DATABASE_POOL_MAX and DATABASE_POOL_TIMEOUT_MS overrides. Do not put Prisma-style connection_limit / pool_timeout on DATABASE_URL — this project uses the PrismaPg driver adapter, and pg ignores those keys.
Email (optional)
# Optional: Resend API key for sending emails
RESEND_API_KEY="re_..."
Authentication
# Better Auth secret key (generate with: openssl rand -base64 32)
BETTER_AUTH_SECRET="your-secret-key-here"
# Base URL for authentication
BETTER_AUTH_URL="http://localhost:3000"
# Optional: extra trusted origins for Capacitor / cross-origin clients
# Capacitor origins are always included; this adds your own (comma-separated)
BETTER_AUTH_TRUSTED_ORIGINS=
# Optional: IPs/CIDRs of proxies in front of the app (comma-separated)
BETTER_AUTH_TRUSTED_PROXIES=
Trusted proxies and rate limiting
better-auth buckets rate limits per client IP, which it reads from x-forwarded-for and then
x-real-ip. It only trusts x-forwarded-for when the header contains a single address, unless
you declare the proxies in front of the app:
# Cloudflare in front of your platform edge, for example
BETTER_AUTH_TRUSTED_PROXIES=173.245.48.0/20,103.21.244.0/22,10.0.0.0/8
Leave it empty when the app is not behind a proxy, or when your proxy replaces x-forwarded-for
with a single address (nginx with proxy_set_header X-Real-IP is fine as-is). If the header carries
multiple hops and no proxies are declared, the client IP cannot be resolved and every request falls
into one shared rate-limit bucket — better-auth logs a warning once when this happens.
App store review accounts
Google Play and App Store reviewers need a fixed username and password, but sign-in here is email +
one-time code. Addresses in REVIEW_ACCOUNT_EMAILS get REVIEW_ACCOUNT_OTP as their sign-in code
and never receive the OTP email:
REVIEW_ACCOUNT_EMAILS=[email protected]
REVIEW_ACCOUNT_OTP=482913
- Set both or neither. With only one set, or a code that doesn't match
OTP_LENGTHandOTP_ALPHABETinshared/config/otp.config.ts, the feature stays off and a warning is logged at startup. - Anyone who knows the email and code can sign in to that account. Clear the variables or rotate the code once review is done.
See mobile apps for what to put in the store forms.
BETTER_AUTH_SECRET to version control. Keep it secure and regenerate it if exposed.Optional variables
Contact form
# Email address(es) to receive contact form submissions (comma-separated for multiple)
CONTACT_FORM_TO_EMAILS="[email protected]"
# Public email address displayed in the contact form
NUXT_PUBLIC_DISPLAY_CONTACT_EMAIL="[email protected]"
Contact form submissions are handled by /api/contact-request/create, stored in the Inquiry table, and then forwarded to configured recipient emails.
Blog content source
# Optional: blog source mode (defaults to "md" when unset)
# md = Nuxt Content markdown files
# db = database-backed blog posts via /api/blog-posts
NUXT_PUBLIC_BLOG_SOURCE="md"
This maps to config.public.blogSource in runtime config.
Stripe payments
# Stripe secret key (server-side)
STRIPE_SECRET_KEY="sk_test_..."
# Stripe publishable key (client-side)
NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."
# Use live Stripe price IDs from payments.config.ts (must be set at build time)
NUXT_PUBLIC_STRIPE_LIVE_MODE="false"
# Webhook signing secret from Stripe dashboard
STRIPE_WEBHOOK_SECRET="whsec_..."
auth or authless) is configured in shared/config/payments.config.ts, not in environment variables. See the payments integration guide for Stripe setup, including NUXT_PUBLIC_STRIPE_LIVE_MODE.AI providers
# OpenAI API key for GPT models
OPENAI_API_KEY="sk-..."
# Anthropic API key for Claude models
ANTHROPIC_API_KEY="sk-ant-..."
# Grok / xAI API key
GROK_API_KEY="xai-..."
Geolocation
# Country detection provider: ipinfo (default) or cloudflare (reads the CF-IPCountry header)
# Read from the server's environment at startup, so switching takes a restart, not a rebuild
NUXT_GEO_PROVIDER="ipinfo"
# IPinfo API token, used by the ipinfo provider (read at build time)
IPINFO_TOKEN="your_token"
Analytics
# Umami Analytics (both required to enable)
NUXT_PUBLIC_UMAMI_ID="your-website-id"
NUXT_PUBLIC_UMAMI_HOST="https://cloud.umami.is"
# Vercel Analytics (set to any truthy value to enable)
NUXT_PUBLIC_VERCEL_ANALYTICS="true"
Variable naming conventions
Nuxt uses specific prefixes for environment variables:
NUXT_PUBLIC_*
Variables with this prefix are exposed to the client-side:
# ✅ Accessible in browser
NUXT_PUBLIC_SITE_URL="https://example.com"
NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."
Access in components:
<script setup>
const config = useRuntimeConfig()
console.log(config.public.siteUrl) // Works in browser
</script>
NUXT_PUBLIC_ for secrets! These are exposed to the browser and visible in the source code.Standard variables
Variables without a special prefix are server-side only:
# ✅ Only accessible on server
DATABASE_URL="postgresql://..."
STRIPE_SECRET_KEY="sk_test_..."
RESEND_API_KEY="re_..."
Access only in server code:
// ✅ Works in server/api routes
const config = useRuntimeConfig()
console.log(config.stripeSecretKey)
// ❌ Returns undefined in browser
Accessing runtime config
All environment variables are mapped to runtime config in nuxt.config.ts.
In server code:
export default defineEventHandler(event => {
const config = useRuntimeConfig()
// Access server-only variables
const dbUrl = config.databaseUrl
const stripeKey = config.stripeSecretKey
// Access public variables
const siteUrl = config.public.siteUrl
})
In client code:
<script setup>
const config = useRuntimeConfig()
// ✅ Public variables work
console.log(config.public.siteUrl)
console.log(config.public.blogSource) // "md" or "db"
// ❌ Server variables are undefined
console.log(config.stripeSecretKey) // undefined
</script>
Environment-specific configuration
You can create different .env files for each environment:
.env- Local development.env.staging- Staging environment.env.production- Production environment
Platform-specific configuration
Vercel
Add environment variables in the Vercel dashboard:
- Go to your project settings
- Navigate to "Environment Variables"
- Add each variable with appropriate scope (Production/Preview/Development)
Netlify
Add in Netlify dashboard or netlify.toml:
[context.production.environment]
NUXT_PUBLIC_SITE_URL = "https://yourdomain.com"
Docker
Use environment variables in docker-compose.yml:
version: '3'
services:
app:
build: .
environment:
- DATABASE_URL=${DATABASE_URL}
- NUXT_PUBLIC_SITE_URL=${NUXT_PUBLIC_SITE_URL}
Or use an .env file:
services:
app:
env_file:
- .env.production
Security best practices
- Never commit
.envfiles - already excluded in.gitignore - Use test keys in development/staging, live keys only in production
- Rotate keys after team changes or if exposed
- Use restricted API keys with minimal required permissions (especially for Stripe and database)
Troubleshooting
Variables not loading
- Check file name: Must be exactly
.env - Restart dev server: Changes require restart
- Check syntax: No spaces around
=
# ❌ Wrong
VARIABLE = "value"
# ✅ Correct
VARIABLE="value"
Public variables returning undefined
Add NUXT_PUBLIC_ prefix:
# ❌ Not accessible in browser
SITE_URL="https://example.com"
# ✅ Accessible in browser
NUXT_PUBLIC_SITE_URL="https://example.com"
Runtime config not updating
Clear Nuxt cache and restart:
rm -rf .nuxt
pnpm dev
Additional resources
Changelog
Maintain the public changelog: where the entry data lives, its shape, the feature, improvement and fix taxonomy, adding a release, and removing the page.
Deployment
Deploy the Nuxt app to production on Vercel, Netlify, Cloudflare Pages or Docker, with a pre-deployment checklist, build settings and custom domains.