Skip to content

Environment Configuration

The Environment Configuration Abstraction Layer provides a unified, type-safe way to access environment variables throughout your application. This guide shows you how to use it effectively.

import { getEnvironmentConfig } from '#utils/env-config'
// Get configuration instance (cached singleton)
const envConfig = getEnvironmentConfig()
// Access environment variables
const isDevelopment = envConfig.isDevelopment()
const clerkKey = envConfig.getClerkPublishableKey()
const supabaseUrl = envConfig.getSupabaseUrl()
// Untyped, unvalidated, error-prone
const clerkKey = import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY
const supabaseUrl = import.meta.env.SUPABASE_URL
// Manual validation required
if (!clerkKey || clerkKey === 'YOUR_CLERK_KEY') {
throw new Error('Invalid configuration')
}

Problems:

  • No type safety
  • No placeholder detection
  • Repeated lookups (slower)
  • Hard to test

Check the current runtime environment:

const envConfig = getEnvironmentConfig()
// Boolean checks
if (envConfig.isDevelopment()) {
console.log('🚧 Running in development mode')
}
if (envConfig.isProduction()) {
console.log('🚀 Running in production')
}
if (envConfig.isTest()) {
console.log('🧪 Running tests')
}
// Get environment string
const env = envConfig.getEnvironment() // 'development' | 'production' | 'test'

Development-only features:

---
import { getEnvironmentConfig } from '#utils/env-config'
const envConfig = getEnvironmentConfig()
const showDebug = envConfig.isDevelopment()
---
{showDebug && (
<div class="debug-panel">
<h3>Debug Info</h3>
<pre>{JSON.stringify(Astro.props, null, 2)}</pre>
</div>
)}

Always validate Clerk configuration before use:

import { getEnvironmentConfig } from '#utils/env-config'
const envConfig = getEnvironmentConfig()
if (!envConfig.isClerkConfigured()) {
throw new Error(
'Clerk not configured. Set PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY'
)
}
// Get individual keys (returns null if not configured)
const publishableKey = envConfig.getClerkPublishableKey()
const secretKey = envConfig.getClerkSecretKey()
const webhookSecret = envConfig.getClerkWebhookSecret()
// Safe pattern with validation
if (envConfig.isClerkConfigured()) {
const publishableKey = envConfig.getClerkPublishableKey()!
const secretKey = envConfig.getClerkSecretKey()!
// Non-null assertions safe after validation
}
src/middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/astro/server'
import { getEnvironmentConfig } from '#utils/env-config'
const envConfig = getEnvironmentConfig()
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/forum(.*)'])
export const onRequest = clerkMiddleware((auth, context) => {
if (!envConfig.isClerkConfigured()) {
throw new Error('Clerk authentication not configured')
}
if (isProtectedRoute(context.request) && !auth().userId) {
return auth().redirectToSignIn()
}
})

import { getEnvironmentConfig } from '#utils/env-config'
const envConfig = getEnvironmentConfig()
// Validation
if (!envConfig.isSupabaseConfigured()) {
throw new Error('Supabase not configured')
}
// Access credentials
const url = envConfig.getSupabaseUrl()!
const anonKey = envConfig.getSupabaseAnonKey()!
const serviceRoleKey = envConfig.getSupabaseServiceRoleKey() // Optional, may be null
import { getEnvironmentConfig } from '#utils/env-config'
const envConfig = getEnvironmentConfig()
// Validation
if (!envConfig.isTursoConfigured()) {
throw new Error('Turso not configured')
}
// Access credentials
const url = envConfig.getTursoDatabaseUrl()!
const token = envConfig.getTursoAuthToken()!
// Check which database is configured
const provider = envConfig.getDatabaseProvider()
// Returns: 'turso' | 'supabase' | 'auto' | null
// Multi-provider support
if (envConfig.isSupabaseConfigured()) {
// Use Supabase
} else if (envConfig.isTursoConfigured()) {
// Use Turso
} else {
throw new Error('No database configured')
}

import { getEnvironmentConfig } from '#utils/env-config'
const envConfig = getEnvironmentConfig()
// Check if Axiom is configured
if (envConfig.isAxiomConfigured()) {
const token = envConfig.getAxiomToken()!
const dataset = envConfig.getAxiomDataset()!
const orgId = envConfig.getAxiomOrgId() // Optional
// Initialize Axiom client
const axiom = new Axiom({
token,
...(orgId ? { orgId } : {})
})
}

import { getEnvironmentStatus } from '#utils/env-config'
const status = getEnvironmentStatus()
console.log(`Environment: ${status.environment}`)
console.log(`Mode: ${status.mode}`)
console.log(`Fully Configured: ${status.isFullyConfigured}`)
// Check specific services
console.log('Services:')
console.log(` Clerk: ${status.services.clerk.configured}`)
console.log(` Database: ${status.services.database.configured}`)
console.log(` Logging: ${status.services.logging.configured}`)
// List missing configuration
if (!status.isFullyConfigured) {
console.warn('Missing configuration:')
status.missingConfiguration.forEach(item => {
console.warn(` - ${item}`)
})
}
{
"environment": "development",
"mode": "development",
"isFullyConfigured": false,
"services": {
"clerk": {
"configured": true,
"hasWebhook": false
},
"database": {
"provider": "supabase",
"configured": true,
"availableProviders": ["supabase"]
},
"logging": {
"configured": false,
"provider": null
}
},
"missingConfiguration": [
"Axiom Logging (AXIOM_TOKEN, AXIOM_DATASET)"
]
}
src/pages/api/health/config.ts
import type { APIRoute } from 'astro'
import { getEnvironmentStatus } from '#utils/env-config'
export const GET: APIRoute = async () => {
const status = getEnvironmentStatus()
return new Response(JSON.stringify(status, null, 2), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
})
}

src/pages/api/webhooks/clerk.ts
import type { APIRoute } from 'astro'
import { Webhook } from 'svix'
import { getEnvironmentConfig } from '#utils/env-config'
const envConfig = getEnvironmentConfig()
export const POST: APIRoute = async ({ request }) => {
const webhookSecret = envConfig.getClerkWebhookSecret()
if (!webhookSecret) {
return new Response(
'Webhook secret not configured',
{ status: 500 }
)
}
const webhook = new Webhook(webhookSecret)
// Verify and process webhook...
return new Response('OK', { status: 200 })
}
src/libs/supabase-auth.ts
import { createClient } from '@supabase/supabase-js'
import { getEnvironmentConfig } from '#utils/env-config'
const envConfig = getEnvironmentConfig()
if (!envConfig.isSupabaseConfigured()) {
throw new Error(
'Supabase not configured. Required: SUPABASE_URL, SUPABASE_ANON_KEY'
)
}
const url = envConfig.getSupabaseUrl()!
const key = envConfig.getSupabaseAnonKey()!
export const supabaseAuth = createClient(url, key, {
auth: {
autoRefreshToken: true,
persistSession: true,
},
})
src/components/EnvironmentBadge.astro
---
import { getEnvironmentConfig } from '#utils/env-config'
const envConfig = getEnvironmentConfig()
const isDev = envConfig.isDevelopment()
const env = envConfig.getEnvironment()
---
{isDev && (
<div class="dev-badge">
🚧 Development Mode ({env})
</div>
)}
<style>
.dev-badge {
position: fixed;
bottom: 1rem;
right: 1rem;
background: #ff6b6b;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-size: 0.875rem;
z-index: 9999;
}
</style>
src/hooks/useSupabase.tsx
import { createClient } from '@supabase/supabase-js'
import { useEffect, useState } from 'react'
import { getEnvironmentConfig } from '#utils/env-config'
export function useSupabase() {
const [client, setClient] = useState<any>(null)
useEffect(() => {
const envConfig = getEnvironmentConfig()
if (envConfig.isSupabaseConfigured()) {
const url = envConfig.getSupabaseUrl()!
const key = envConfig.getSupabaseAnonKey()!
const supabase = createClient(url, key)
setClient(supabase)
} else {
console.warn('Supabase not configured')
}
}, [])
return client
}

import { describe, it, expect, vi } from 'vitest'
import { getEnvironmentConfig } from '#utils/env-config'
// Mock the factory function
vi.mock('#utils/env-config', () => ({
getEnvironmentConfig: vi.fn(() => ({
isClerkConfigured: () => true,
getClerkPublishableKey: () => 'pk_test_mock_key',
getClerkSecretKey: () => 'sk_test_mock_secret',
isDevelopment: () => true,
isProduction: () => false,
})),
}))
describe('Middleware', () => {
it('should authenticate with valid Clerk keys', () => {
const config = getEnvironmentConfig()
expect(config.isClerkConfigured()).toBe(true)
expect(config.getClerkPublishableKey()).toBe('pk_test_mock_key')
})
})
import { getEnvironmentConfig } from '#utils/env-config'
describe('Clerk Integration', () => {
it('should work with real Clerk credentials', () => {
const envConfig = getEnvironmentConfig()
// Skip test if not configured
if (!envConfig.isClerkConfigured()) {
console.warn('Skipping: Clerk not configured')
return
}
// Run integration test with real credentials
const publishableKey = envConfig.getClerkPublishableKey()
expect(publishableKey).toMatch(/^pk_(test|live)_/)
})
})

Terminal window
# Clerk Authentication (Required)
PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
CLERK_SECRET_KEY=sk_test_your_secret_here
Terminal window
# Clerk (Optional)
CLERK_WEBHOOK_SECRET=whsec_your_webhook_secret
# Database Provider Selection (Optional)
DATABASE_PROVIDER=supabase # 'turso' | 'supabase' | 'auto'
# Axiom Logging (Optional)
AXIOM_TOKEN=your_axiom_token
AXIOM_DATASET=your_dataset_name
AXIOM_ORG_ID=your_org_id # Optional
# Astro (Optional)
ASTRO_ADAPTER=netlify # 'netlify' | 'node' | 'vercel'
PUBLIC_SITE_URL=https://example.com

To obtain your Supabase credentials, follow these steps:

1. Open Supabase Dashboard

2. Access API Settings

  • Click Settings (gear icon) in the left sidebar
  • Navigate to the API section

3. Copy Your Credentials

Credential Location Variable Name
Project URL Top of API page SUPABASE_URL
anon (public) key Project API Keys section SUPABASE_ANON_KEY
service_role (secret) key Project API Keys section SUPABASE_SERVICE_ROLE_KEY

For detailed setup instructions with screenshots, see the Complete Setup Guide.

To obtain your Clerk credentials:

1. Open Clerk Dashboard

2. Access API Keys

  • Click API Keys in the left sidebar

3. Copy Your Credentials

Credential Format Variable Name
Publishable Key Starts with pk_test_ or pk_live_ PUBLIC_CLERK_PUBLISHABLE_KEY
Secret Key Starts with sk_test_ or sk_live_ CLERK_SECRET_KEY

4. Optional: Webhook Secret

  • Navigate to Webhooks → Select your endpoint
  • Copy the Signing SecretCLERK_WEBHOOK_SECRET

To obtain Turso credentials using the CLI:

1. Install Turso CLI

Terminal window
curl -sSfL https://get.tur.so/install.sh | bash

2. Authenticate

Terminal window
turso auth login

3. Create or Select Database

Terminal window
# Create new database
turso db create my-database
# Or list existing databases
turso db list

4. Get Connection Details

Terminal window
# Get database URL → TURSO_DATABASE_URL
turso db show my-database --url
# Create auth token → TURSO_AUTH_TOKEN
turso db tokens create my-database

1. Use validation helpers before accessing values:

if (envConfig.isClerkConfigured()) {
const key = envConfig.getClerkPublishableKey()!
// Safe to use non-null assertion
}

2. Create instance at module level for performance:

// Top of file
import { getEnvironmentConfig } from '#utils/env-config'
const envConfig = getEnvironmentConfig()
// Use throughout module
export function myFunction() {
const isDev = envConfig.isDevelopment()
}

3. Provide descriptive error messages:

if (!envConfig.isSupabaseConfigured()) {
throw new Error(
'Supabase not configured. Set SUPABASE_URL and SUPABASE_ANON_KEY in .env'
)
}

1. Don’t access import.meta.env directly:

const key = import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY
const key = envConfig.getClerkPublishableKey()

2. Don’t skip validation checks:

const key = envConfig.getClerkPublishableKey()! // Might be null!
if (envConfig.isClerkConfigured()) { ... }

3. Don’t create multiple instances:

function doSomething() {
const config = getEnvironmentConfig() // New instance each call
}
const envConfig = getEnvironmentConfig() // Module level, reused
function doSomething() {
const isDev = envConfig.isDevelopment()
}

Error:

Error: Clerk not configured

Solution:

  1. Check your .env file exists and has the required variables
  2. Verify values are not placeholders (YOUR_CLERK_KEY)
  3. Check environment status:
import { getEnvironmentStatus } from '#utils/env-config'
console.log(getEnvironmentStatus())

Error:

Error: Invalid Clerk key format

Cause: Using placeholder like YOUR_CLERK_PUBLISHABLE_KEY

Solution: The abstraction automatically detects these. Check validation:

const envConfig = getEnvironmentConfig()
console.log(envConfig.isClerkConfigured()) // Should be false with placeholders

Update your .env file with real credentials:

Terminal window
PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_real_key_here
CLERK_SECRET_KEY=sk_test_real_secret_here

Error:

TypeError: Cannot read property 'getClerkPublishableKey' of undefined

Cause: Mock not configured properly

Solution:

vi.mock('#utils/env-config', () => ({
getEnvironmentConfig: vi.fn(() => ({
isClerkConfigured: () => true,
getClerkPublishableKey: () => 'test_key',
getClerkSecretKey: () => 'test_secret',
// Add all methods your tests need
})),
}))


★ Insight ─────────────────────────────────────

Performance Through Caching: The environment configuration abstraction uses a singleton pattern with lazy loading. The first call loads and caches all environment variables (~0.2ms), but subsequent calls simply return the cached instance (~0.05ms). This makes it faster than repeated direct access to import.meta.env.

Type Safety Benefits: TypeScript’s type system prevents you from accessing non-existent environment variables at compile time. This catches configuration typos during development rather than at runtime in production.

Testing Simplicity: Mocking getEnvironmentConfig() is dramatically simpler than mocking import.meta.env globally. You can create different configurations for different tests easily, improving test isolation and reliability.

─────────────────────────────────────────────────