Configuration
Better SaaS uses a centralized configuration system that manages application settings, feature flags, environment variables, and runtime configuration with type safety and validation.
Overview
The configuration system provides:
- Environment Management: Development, staging, and production environments
- Feature Flags: Dynamic feature toggling and A/B testing
- Type Safety: TypeScript validation for all configuration
- Runtime Configuration: Dynamic settings without rebuilding
- Validation: Zod schemas for configuration validation
- Centralized Management: Single source of truth for settings
Environment Variables
Environment Files Structure
.env.local # Local development (not committed)
.env.example # Template for environment variables
.env.production # Production environment variables
.env.staging # Staging environment variables
Core Environment Variables
# Application
NODE_ENV=development
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_APP_NAME="Better SaaS"
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/bettersaas
# Authentication
BETTER_AUTH_SECRET=your-super-secret-key-here
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000
# OAuth Providers
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Payment (Stripe)
STRIPE_SECRET_KEY=sk_test_your-test-secret-key
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your-test-publishable-key
STRIPE_WEBHOOK_SECRET=whsec_your-webhook-secret
# File Storage (Cloudflare R2)
CLOUDFLARE_R2_ACCESS_KEY_ID=your-access-key-id
CLOUDFLARE_R2_SECRET_ACCESS_KEY=your-secret-access-key
CLOUDFLARE_R2_BUCKET_NAME=your-bucket-name
CLOUDFLARE_R2_ENDPOINT=https://your-account-id.r2.cloudflarestorage.com
NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_URL=https://your-domain.com
# Email (Resend)
RESEND_API_KEY=your-resend-api-key
RESEND_FROM_EMAIL=noreply@yourdomain.com
# Analytics (Optional)
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=GA_MEASUREMENT_ID
NEXT_PUBLIC_POSTHOG_KEY=your-posthog-key
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
# Monitoring (Optional)
SENTRY_DSN=your-sentry-dsn
SENTRY_ORG=your-sentry-org
SENTRY_PROJECT=your-sentry-project
# Feature Flags
NEXT_PUBLIC_ENABLE_ANALYTICS=true
NEXT_PUBLIC_ENABLE_BLOG=true
NEXT_PUBLIC_ENABLE_DOCS=true
NEXT_PUBLIC_ENABLE_PAYMENTS=true
Environment Validation
// src/env.ts
import { z } from 'zod'
const envSchema = z.object({
// Application
NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
NEXT_PUBLIC_APP_URL: z.string().url(),
NEXT_PUBLIC_APP_NAME: z.string().default('Better SaaS'),
// Database
DATABASE_URL: z.string().url(),
// Authentication
BETTER_AUTH_SECRET: z.string().min(32),
BETTER_AUTH_URL: z.string().url(),
BETTER_AUTH_TRUSTED_ORIGINS: z.string().optional(),
// OAuth
GITHUB_CLIENT_ID: z.string().optional(),
GITHUB_CLIENT_SECRET: z.string().optional(),
GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),
// Payment
STRIPE_SECRET_KEY: z.string().optional(),
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().optional(),
STRIPE_WEBHOOK_SECRET: z.string().optional(),
// File Storage
CLOUDFLARE_R2_ACCESS_KEY_ID: z.string().optional(),
CLOUDFLARE_R2_SECRET_ACCESS_KEY: z.string().optional(),
CLOUDFLARE_R2_BUCKET_NAME: z.string().optional(),
CLOUDFLARE_R2_ENDPOINT: z.string().url().optional(),
NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_URL: z.string().url().optional(),
// Email
RESEND_API_KEY: z.string().optional(),
RESEND_FROM_EMAIL: z.string().email().optional(),
// Analytics
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID: z.string().optional(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_HOST: z.string().url().optional(),
// Monitoring
SENTRY_DSN: z.string().url().optional(),
SENTRY_ORG: z.string().optional(),
SENTRY_PROJECT: z.string().optional(),
// Feature Flags
NEXT_PUBLIC_ENABLE_ANALYTICS: z.string().transform(val => val === 'true').default('false'),
NEXT_PUBLIC_ENABLE_BLOG: z.string().transform(val => val === 'true').default('true'),
NEXT_PUBLIC_ENABLE_DOCS: z.string().transform(val => val === 'true').default('true'),
NEXT_PUBLIC_ENABLE_PAYMENTS: z.string().transform(val => val === 'true').default('true'),
})
export const env = envSchema.parse(process.env)
export type Env = z.infer<typeof envSchema>
Application Configuration
Core App Configuration
// src/config/app.config.ts
import { env } from '@/env'
export const APP_CONFIG = {
name: env.NEXT_PUBLIC_APP_NAME,
url: env.NEXT_PUBLIC_APP_URL,
description: 'Modern SaaS application built with Next.js',
// Application metadata
meta: {
title: 'Better SaaS - Modern SaaS Template',
description: 'Build your SaaS application with Next.js, TypeScript, and modern tools',
keywords: ['SaaS', 'Next.js', 'TypeScript', 'React', 'Tailwind CSS'],
author: 'Better SaaS Team',
version: '1.0.0',
},
// Social media and branding
social: {
twitter: '@bettersaas',
github: 'https://github.com/yourusername/better-saas',
discord: 'https://discord.gg/yourinvite',
},
// Contact information
contact: {
email: 'hello@bettersaas.com',
support: 'support@bettersaas.com',
},
// Legal
legal: {
company: 'Better SaaS Inc.',
address: '123 Main St, San Francisco, CA 94105',
privacyPolicy: '/privacy',
termsOfService: '/terms',
},
// Default settings
defaults: {
locale: 'en',
theme: 'light',
currency: 'USD',
timezone: 'UTC',
},
} as const
export type AppConfig = typeof APP_CONFIG
Feature Configuration
// src/config/features.config.ts
import { env } from '@/env'
export const FEATURES_CONFIG = {
// Core features
authentication: {
enabled: true,
providers: {
email: true,
github: !!env.GITHUB_CLIENT_ID,
google: !!env.GOOGLE_CLIENT_ID,
},
features: {
registration: true,
emailVerification: false,
twoFactorAuth: false,
passwordReset: true,
},
},
// Payment features
payments: {
enabled: env.NEXT_PUBLIC_ENABLE_PAYMENTS,
provider: 'stripe',
features: {
subscriptions: true,
oneTimePayments: true,
invoices: true,
refunds: true,
},
plans: {
basic: {
id: 'basic',
name: 'Basic',
price: 9.99,
interval: 'month',
features: ['5 projects', '10GB storage', 'Email support'],
},
pro: {
id: 'pro',
name: 'Pro',
price: 29.99,
interval: 'month',
features: ['Unlimited projects', '100GB storage', 'Priority support'],
},
},
},
// File management
fileManagement: {
enabled: true,
provider: 'r2',
limits: {
maxFileSize: 10 * 1024 * 1024, // 10MB
maxFiles: 1000,
allowedTypes: ['image/jpeg', 'image/png', 'application/pdf'],
},
features: {
thumbnails: true,
sharing: true,
folders: true,
search: true,
},
},
// Analytics
analytics: {
enabled: env.NEXT_PUBLIC_ENABLE_ANALYTICS,
providers: {
googleAnalytics: !!env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID,
posthog: !!env.NEXT_PUBLIC_POSTHOG_KEY,
},
},
// Content features
content: {
blog: {
enabled: env.NEXT_PUBLIC_ENABLE_BLOG,
features: {
comments: false,
rss: true,
search: true,
categories: true,
},
},
docs: {
enabled: env.NEXT_PUBLIC_ENABLE_DOCS,
features: {
search: true,
feedback: true,
editOnGithub: true,
},
},
},
// Admin features
admin: {
enabled: true,
features: {
userManagement: true,
analytics: true,
systemSettings: true,
auditLog: true,
},
},
// Internationalization
i18n: {
enabled: true,
defaultLocale: 'en',
locales: ['en', 'zh'],
features: {
autoDetect: true,
localizedRouting: true,
},
},
} as const
export type FeaturesConfig = typeof FEATURES_CONFIG
Theme Configuration
// src/config/theme.config.ts
export const THEME_CONFIG = {
// Color schemes
colors: {
light: {
primary: 'hsl(221, 83%, 53%)',
secondary: 'hsl(210, 40%, 98%)',
accent: 'hsl(210, 40%, 96%)',
background: 'hsl(0, 0%, 100%)',
foreground: 'hsl(222, 84%, 5%)',
muted: 'hsl(210, 40%, 96%)',
border: 'hsl(214, 32%, 91%)',
},
dark: {
primary: 'hsl(221, 83%, 53%)',
secondary: 'hsl(217, 33%, 17%)',
accent: 'hsl(217, 33%, 17%)',
background: 'hsl(222, 84%, 5%)',
foreground: 'hsl(210, 40%, 98%)',
muted: 'hsl(217, 33%, 17%)',
border: 'hsl(217, 33%, 17%)',
},
},
// Typography
typography: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['Fira Code', 'monospace'],
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
},
},
// Layout
layout: {
containerMaxWidth: '1200px',
sidebarWidth: '240px',
headerHeight: '64px',
footerHeight: '80px',
},
// Animation
animation: {
duration: {
fast: '150ms',
normal: '300ms',
slow: '500ms',
},
easing: {
default: 'cubic-bezier(0.4, 0, 0.2, 1)',
in: 'cubic-bezier(0.4, 0, 1, 1)',
out: 'cubic-bezier(0, 0, 0.2, 1)',
},
},
// Breakpoints
breakpoints: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
},
} as const
export type ThemeConfig = typeof THEME_CONFIG
Navigation Configuration
// src/config/navbar.config.ts
import { FEATURES_CONFIG } from './features.config'
export const NAVBAR_CONFIG = {
// Main navigation
main: [
{
title: 'Home',
href: '/',
icon: 'home',
},
{
title: 'Features',
href: '/features',
icon: 'features',
},
{
title: 'Pricing',
href: '/pricing',
icon: 'pricing',
},
...(FEATURES_CONFIG.content.blog.enabled ? [{
title: 'Blog',
href: '/blog',
icon: 'blog',
}] : []),
...(FEATURES_CONFIG.content.docs.enabled ? [{
title: 'Docs',
href: '/docs',
icon: 'docs',
}] : []),
],
// Dashboard navigation
dashboard: [
{
title: 'Overview',
href: '/dashboard',
icon: 'dashboard',
},
{
title: 'Projects',
href: '/dashboard/projects',
icon: 'projects',
},
...(FEATURES_CONFIG.fileManagement.enabled ? [{
title: 'Files',
href: '/dashboard/files',
icon: 'files',
}] : []),
...(FEATURES_CONFIG.payments.enabled ? [{
title: 'Billing',
href: '/dashboard/billing',
icon: 'billing',
}] : []),
{
title: 'Settings',
href: '/dashboard/settings',
icon: 'settings',
},
],
// Admin navigation
admin: [
{
title: 'Users',
href: '/admin/users',
icon: 'users',
permission: 'manage:users',
},
{
title: 'Analytics',
href: '/admin/analytics',
icon: 'analytics',
permission: 'view:analytics',
},
{
title: 'Settings',
href: '/admin/settings',
icon: 'settings',
permission: 'manage:settings',
},
],
// Footer navigation
footer: {
product: [
{ title: 'Features', href: '/features' },
{ title: 'Pricing', href: '/pricing' },
{ title: 'Changelog', href: '/changelog' },
],
company: [
{ title: 'About', href: '/about' },
{ title: 'Contact', href: '/contact' },
{ title: 'Careers', href: '/careers' },
],
legal: [
{ title: 'Privacy', href: '/privacy' },
{ title: 'Terms', href: '/terms' },
{ title: 'Security', href: '/security' },
],
},
} as const
export type NavbarConfig = typeof NAVBAR_CONFIG
Payment Configuration
// src/config/payment.config.ts
import { env } from '@/env'
export const PAYMENT_CONFIG = {
// Provider configuration
provider: 'stripe',
enabled: env.NEXT_PUBLIC_ENABLE_PAYMENTS,
// Currency settings
currency: 'USD',
locale: 'en-US',
// Subscription plans
plans: {
basic: {
id: 'basic',
name: 'Basic',
description: 'Perfect for individuals and small teams',
price: 9.99,
interval: 'month',
stripePriceId: 'price_basic_monthly',
features: [
'Up to 5 projects',
'10GB storage',
'Email support',
'Basic analytics',
],
limits: {
projects: 5,
storage: 10 * 1024 * 1024 * 1024, // 10GB
users: 1,
apiCalls: 1000,
},
},
pro: {
id: 'pro',
name: 'Pro',
description: 'For growing teams and businesses',
price: 29.99,
interval: 'month',
stripePriceId: 'price_pro_monthly',
features: [
'Unlimited projects',
'100GB storage',
'Priority support',
'Advanced analytics',
'Team collaboration',
],
limits: {
projects: -1, // Unlimited
storage: 100 * 1024 * 1024 * 1024, // 100GB
users: 10,
apiCalls: 10000,
},
},
enterprise: {
id: 'enterprise',
name: 'Enterprise',
description: 'For large organizations with advanced needs',
price: 99.99,
interval: 'month',
stripePriceId: 'price_enterprise_monthly',
features: [
'Everything in Pro',
'1TB storage',
'Dedicated support',
'Custom integrations',
'SLA guarantee',
],
limits: {
projects: -1, // Unlimited
storage: 1024 * 1024 * 1024 * 1024, // 1TB
users: -1, // Unlimited
apiCalls: -1, // Unlimited
},
},
},
// Billing settings
billing: {
trialPeriod: 14, // days
gracePeriod: 3, // days
invoicePrefix: 'INV-',
taxRate: 0.1, // 10%
},
// Feature access by plan
features: {
basic: [
'projects',
'storage',
'support',
'analytics',
],
pro: [
'projects',
'storage',
'support',
'analytics',
'collaboration',
'integrations',
],
enterprise: [
'projects',
'storage',
'support',
'analytics',
'collaboration',
'integrations',
'sla',
'custom',
],
},
} as const
export type PaymentConfig = typeof PAYMENT_CONFIG
export type PlanId = keyof typeof PAYMENT_CONFIG.plans
export type Plan = typeof PAYMENT_CONFIG.plans[PlanId]
Configuration Validation
Runtime Configuration Validation
// src/lib/config-validation.ts
import { z } from 'zod'
// Configuration schemas
const appConfigSchema = z.object({
name: z.string().min(1),
url: z.string().url(),
description: z.string().min(1),
meta: z.object({
title: z.string().min(1),
description: z.string().min(1),
keywords: z.array(z.string()),
author: z.string().min(1),
version: z.string().min(1),
}),
})
const featuresConfigSchema = z.object({
authentication: z.object({
enabled: z.boolean(),
providers: z.object({
email: z.boolean(),
github: z.boolean(),
google: z.boolean(),
}),
}),
payments: z.object({
enabled: z.boolean(),
provider: z.enum(['stripe', 'paddle']),
}),
fileManagement: z.object({
enabled: z.boolean(),
provider: z.enum(['r2', 's3']),
limits: z.object({
maxFileSize: z.number().positive(),
maxFiles: z.number().positive(),
allowedTypes: z.array(z.string()),
}),
}),
})
// Validation functions
export function validateAppConfig(config: unknown) {
return appConfigSchema.parse(config)
}
export function validateFeaturesConfig(config: unknown) {
return featuresConfigSchema.parse(config)
}
// Configuration loader with validation
export function loadConfig<T>(
configLoader: () => T,
validator: (config: unknown) => T
): T {
try {
const config = configLoader()
return validator(config)
} catch (error) {
console.error('Configuration validation failed:', error)
throw new Error('Invalid configuration')
}
}
Configuration Hooks
useConfig Hook
// src/hooks/use-config.ts
import { useContext, createContext, ReactNode } from 'react'
import { APP_CONFIG, AppConfig } from '@/config/app.config'
import { FEATURES_CONFIG, FeaturesConfig } from '@/config/features.config'
import { THEME_CONFIG, ThemeConfig } from '@/config/theme.config'
interface ConfigContextType {
app: AppConfig
features: FeaturesConfig
theme: ThemeConfig
}
const ConfigContext = createContext<ConfigContextType | undefined>(undefined)
export function ConfigProvider({ children }: { children: ReactNode }) {
const config = {
app: APP_CONFIG,
features: FEATURES_CONFIG,
theme: THEME_CONFIG,
}
return (
<ConfigContext.Provider value={config}>
{children}
</ConfigContext.Provider>
)
}
export function useConfig() {
const context = useContext(ConfigContext)
if (!context) {
throw new Error('useConfig must be used within a ConfigProvider')
}
return context
}
// Specific config hooks
export function useAppConfig() {
const { app } = useConfig()
return app
}
export function useFeaturesConfig() {
const { features } = useConfig()
return features
}
export function useThemeConfig() {
const { theme } = useConfig()
return theme
}
Feature Flags
Feature Flag System
// src/lib/feature-flags.ts
import { FEATURES_CONFIG } from '@/config/features.config'
export type FeatureFlag = keyof typeof FEATURES_CONFIG
export function isFeatureEnabled(feature: FeatureFlag): boolean {
const config = FEATURES_CONFIG[feature]
return typeof config === 'object' && 'enabled' in config
? config.enabled
: Boolean(config)
}
export function getFeatureConfig<T extends FeatureFlag>(
feature: T
): typeof FEATURES_CONFIG[T] {
return FEATURES_CONFIG[feature]
}
// Feature flag component
export function FeatureFlag({
feature,
children,
fallback = null
}: {
feature: FeatureFlag
children: React.ReactNode
fallback?: React.ReactNode
}) {
if (!isFeatureEnabled(feature)) {
return <>{fallback}</>
}
return <>{children}</>
}
// Feature flag hook
export function useFeatureFlag(feature: FeatureFlag) {
return isFeatureEnabled(feature)
}
Configuration Management
Dynamic Configuration
// src/lib/dynamic-config.ts
import { useState, useEffect } from 'react'
interface DynamicConfig {
maintenance: boolean
announcements: string[]
experimentalFeatures: Record<string, boolean>
}
export function useDynamicConfig() {
const [config, setConfig] = useState<DynamicConfig>({
maintenance: false,
announcements: [],
experimentalFeatures: {},
})
useEffect(() => {
// Load dynamic configuration from API
fetch('/api/config')
.then(res => res.json())
.then(setConfig)
.catch(console.error)
}, [])
return config
}
// Server-side dynamic config
export async function getDynamicConfig(): Promise<DynamicConfig> {
// This could fetch from a database, external API, or config service
return {
maintenance: process.env.MAINTENANCE_MODE === 'true',
announcements: process.env.ANNOUNCEMENTS?.split(',') || [],
experimentalFeatures: {
newDashboard: process.env.EXPERIMENTAL_NEW_DASHBOARD === 'true',
betaFeatures: process.env.EXPERIMENTAL_BETA_FEATURES === 'true',
},
}
}
Configuration API
// src/app/api/config/route.ts
import { NextResponse } from 'next/server'
import { getDynamicConfig } from '@/lib/dynamic-config'
export async function GET() {
try {
const config = await getDynamicConfig()
return NextResponse.json(config)
} catch (error) {
return NextResponse.json(
{ error: 'Failed to load configuration' },
{ status: 500 }
)
}
}
Environment-Specific Configuration
Development Configuration
// src/config/development.config.ts
export const DEVELOPMENT_CONFIG = {
debug: true,
logging: {
level: 'debug',
console: true,
file: false,
},
database: {
logging: true,
ssl: false,
},
cache: {
enabled: false,
},
rateLimit: {
enabled: false,
},
} as const
Production Configuration
// src/config/production.config.ts
export const PRODUCTION_CONFIG = {
debug: false,
logging: {
level: 'error',
console: false,
file: true,
},
database: {
logging: false,
ssl: true,
},
cache: {
enabled: true,
ttl: 3600,
},
rateLimit: {
enabled: true,
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
},
} as const
Testing Configuration
Test Configuration
// src/config/test.config.ts
export const TEST_CONFIG = {
database: {
url: 'postgresql://test:test@localhost:5432/bettersaas_test',
logging: false,
},
auth: {
secret: 'test-secret-key',
skipVerification: true,
},
storage: {
provider: 'memory',
},
email: {
provider: 'mock',
},
} as const
Best Practices
Configuration Organization
- Separate Concerns: Keep different types of configuration in separate files
- Type Safety: Use TypeScript for all configuration
- Validation: Validate configuration at startup
- Environment Variables: Use environment variables for sensitive data
- Defaults: Provide sensible defaults for all configuration
Security Considerations
- Secret Management: Never commit secrets to version control
- Environment Separation: Use different configurations for different environments
- Validation: Validate all configuration inputs
- Access Control: Limit access to configuration endpoints
Performance Optimization
- Lazy Loading: Load configuration only when needed
- Caching: Cache configuration to avoid repeated reads
- Bundling: Optimize configuration for client-side bundling
Troubleshooting
Common Issues
- Missing Environment Variables: Check
.env.example
for required variables - Invalid Configuration: Use validation schemas to catch errors early
- Type Errors: Ensure all configuration is properly typed
- Runtime Errors: Validate configuration at application startup
Debug Tools
// src/lib/config-debug.ts
export function debugConfiguration() {
if (process.env.NODE_ENV === 'development') {
console.log('App Config:', APP_CONFIG)
console.log('Features Config:', FEATURES_CONFIG)
console.log('Environment Variables:', process.env)
}
}