Configuration
On this page 18
Stacks provides a powerful, type-safe configuration system that centralizes all your application settings. Configure everything from database connections to cloud deployments in one place.
Overview
Configuration in Stacks follows these principles:
- Type-safe - Full TypeScript support with autocompletion
- Environment-aware - Different configs for dev, staging, production
- Centralized - All settings in one organized location
- Validated - Configuration errors caught at startup
Configuration Files
All configuration lives in the config/ directory:
config/
├── app.ts # Application settings
├── database.ts # Database connections
├── cache.ts # Cache configuration
├── queue.ts # Queue settings
├── mail.ts # Email configuration
├── storage.ts # File storage
├── auth.ts # Authentication
├── cloud.ts # Cloud deployment
├── hashing.ts # Password hashing
├── logging.ts # Log configuration
├── services.ts # Third-party services
├── server.ts # Views server / API proxy
└── index.ts # Config export
Application Config
// config/app.ts
import { defineApp } from '@stacksjs/config'
export default defineApp({
name: 'My App',
env: process.env.APP_ENV || 'development',
debug: process.env.APP_DEBUG === 'true',
url: process.env.APP_URL || 'http://localhost:3000',
timezone: 'UTC',
locale: 'en',
key: process.env.APP_KEY,
providers: [
// Service providers to load
],
})
Database Config
// config/database.ts
import { defineDatabase } from '@stacksjs/config'
export default defineDatabase({
default: process.env.DB_CONNECTION || 'sqlite',
connections: {
sqlite: {
driver: 'sqlite',
database: process.env.DB_DATABASE || 'database/database.sqlite',
},
mysql: {
driver: 'mysql',
host: process.env.DB_HOST || '127.0.0.1',
port: Number(process.env.DB_PORT) || 3306,
database: process.env.DB_DATABASE || 'stacks',
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
charset: 'utf8mb4',
collation: 'utf8mb4_unicode_ci',
},
postgres: {
driver: 'postgres',
host: process.env.DB_HOST || '127.0.0.1',
port: Number(process.env.DB_PORT) || 5432,
database: process.env.DB_DATABASE || 'stacks',
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || '',
schema: 'public',
},
},
migrations: {
table: 'migrations',
path: 'database/migrations',
},
seeders: {
path: 'database/seeders',
},
})
Cache Config
// config/cache.ts
import { defineCache } from '@stacksjs/config'
export default defineCache({
default: process.env.CACHE_DRIVER || 'file',
stores: {
file: {
driver: 'file',
path: 'storage/cache',
},
redis: {
driver: 'redis',
connection: 'cache',
prefix: 'cache:',
},
memory: {
driver: 'memory',
maxSize: 100 _ 1024 _ 1024, // 100MB
},
dynamodb: {
driver: 'dynamodb',
table: process.env.DYNAMODB_CACHE_TABLE || 'cache',
region: process.env.AWS_REGION || 'us-east-1',
},
},
prefix: process.env.CACHE_PREFIX || 'stacks_cache_',
ttl: 3600, // 1 hour default
})
Queue Config
// config/queue.ts
import { defineQueue } from '@stacksjs/config'
export default defineQueue({
default: process.env.QUEUE_CONNECTION || 'sync',
connections: {
sync: {
driver: 'sync',
},
database: {
driver: 'database',
table: 'jobs',
queue: 'default',
retryAfter: 90,
},
redis: {
driver: 'redis',
connection: 'default',
queue: 'default',
retryAfter: 90,
blockFor: 5,
},
sqs: {
driver: 'sqs',
key: process.env.AWS_ACCESS_KEY_ID,
secret: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION || 'us-east-1',
prefix: process.env.SQS_PREFIX,
queue: process.env.SQS_QUEUE || 'default',
},
},
failed: {
driver: 'database',
table: 'failed_jobs',
},
})
Mail Config
// config/mail.ts
import { defineEmail } from '@stacksjs/config'
export default defineEmail({
default: process.env.MAIL_MAILER || 'smtp',
mailers: {
smtp: {
driver: 'smtp',
host: process.env.MAIL_HOST || 'localhost',
port: Number(process.env.MAIL_PORT) || 587,
encryption: process.env.MAIL_ENCRYPTION || 'tls',
username: process.env.MAIL_USERNAME,
password: process.env.MAIL_PASSWORD,
},
ses: {
driver: 'ses',
region: process.env.AWS_REGION || 'us-east-1',
},
mailgun: {
driver: 'mailgun',
domain: process.env.MAILGUN_DOMAIN,
secret: process.env.MAILGUN_SECRET,
endpoint: process.env.MAILGUN_ENDPOINT || 'api.mailgun.net',
},
log: {
driver: 'log',
channel: 'mail',
},
},
from: {
address: process.env.MAIL_FROM_ADDRESS || 'hello@example.com',
name: process.env.MAIL_FROM_NAME || 'Stacks App',
},
})
Storage Config
// config/storage.ts
import { defineStorage } from '@stacksjs/config'
export default defineStorage({
default: process.env.FILESYSTEM_DISK || 'local',
disks: {
local: {
driver: 'local',
root: 'storage/app',
visibility: 'private',
},
public: {
driver: 'local',
root: 'storage/app/public',
url: '/storage',
visibility: 'public',
},
s3: {
driver: 's3',
bucket: process.env.AWS_BUCKET,
region: process.env.AWS_REGION || 'us-east-1',
url: process.env.AWS_URL,
endpoint: process.env.AWS_ENDPOINT,
forcePathStyle: process.env.AWS_USE_PATH_STYLE === 'true',
},
},
links: {
'public/storage': 'storage/app/public',
},
})
Auth Config
// config/auth.ts
import { defineAuth } from '@stacksjs/config'
export default defineAuth({
defaults: {
guard: 'web',
provider: 'users',
},
guards: {
web: {
driver: 'session',
provider: 'users',
},
api: {
driver: 'token',
provider: 'users',
hash: false,
},
},
providers: {
users: {
driver: 'database',
model: 'User',
},
},
passwords: {
users: {
provider: 'users',
table: 'password_reset_tokens',
expire: 60,
throttle: 60,
},
},
session: {
lifetime: 120,
expireOnClose: false,
},
})
Server Config
config/server.ts controls the views server in the split views/API topology,
which is what both buddy dev and buddy serve run.
The views server renders stx pages and forwards everything else to the API
process. By default "everything else" means the /api/** prefix plus the
mutating verbs, which never match a page render:
export default {
proxy: {
prefixes: [],
paths: [],
},
} satisfies ServerConfig
That default leaves a plain GET route declared at the root on the API process
unreachable, because the views server treats it as a page and 404s. Name it here
and it is forwarded:
export default {
proxy: {
// Exact paths, forwarded whatever the verb.
paths: ['/health', '/me'],
// Prefixes, forwarded along with their whole subtree.
// `/api/` is always forwarded and does not need listing.
prefixes: ['/oauth/'],
// Verbs forwarded whatever the path. Setting this REPLACES the default
// of POST/PUT/PATCH/DELETE rather than adding to it.
// methods: ['POST', 'PUT', 'PATCH', 'DELETE'],
},
} satisfies ServerConfig
buddy dev prints the effective rules at boot whenever you have widened them,
so a 404 on a route you know you registered is diagnosable.
One caveat before adding a path: stx runs its request hook before static file
serving, so a path listed here shadows a public/ file of the same name.
Listing /script.js makes public/script.js unreachable.
Why this is configuration rather than a route lookup: the route table is
registered in the API process, which is a separate process under buddy dev
and potentially a separate host in production, so the views server cannot
consult it.
Security headers on pages
Every response the views server renders carries the same three headers the API already sent:
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
None of them can be set from a template. X-Frame-Options is an HTTP header
with no <meta> equivalent, and CSP frame-ancestors is ignored when set
through <meta http-equiv>, so this has to come from the server.
An app that serves a deliberately embeddable page names it, and only
X-Frame-Options is dropped for those paths:
export default {
security: {
// An entry ending in `/` is a prefix. Anything else is an exact path.
embeddable: ['/embed/', '/share/card'],
},
} satisfies ServerConfig
Both buddy dev and buddy serve print the list at boot, because a page
another origin can frame is a deliberate exception worth seeing.
Two headers are deliberately not sent on pages:
Content-Security-Policy.STACKS_CSPhas only ever reached JSON API responses, and a blanket policy breaks inline stx script bootstrapping, Stripe iframes and OAuth popups. A page policy is worth having, but it needs its own testing rather than arriving as a side effect.Strict-Transport-Security.buddy servetreats itself as production even on a laptop, and HSTS onlocalhostcommits your browser to HTTPS for that host for a year. Terminate TLS at a proxy and set it there.
STACKS_SECURITY_HEADERS_DISABLE=true turns the whole set off, on both the API
and the views server, for a deployment behind a proxy that injects its own.
Environment Variables
Stacks uses a .env file for environment-specific configuration:
# .env
APP_NAME="My Stacks App"
APP_ENV=development
APP_KEY=base64:...
APP_DEBUG=true
APP_URL=http://localhost:3000
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=stacks
DB_USERNAME=root
DB_PASSWORD=
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=null
MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
Accessing Configuration
import { config } from '@stacksjs/config'
// Get a value
const appName = config('app.name')
const dbHost = config('database.connections.mysql.host')
// Get with default
const timezone = config('app.timezone', 'UTC')
// Check if config exists
if (config.has('services.stripe')) {
// Stripe is configured
}
// Get entire section
const mailConfig = config('mail')
Environment Helpers
import { env, isProduction, isDevelopment, isLocal } from '@stacksjs/env'
// Get environment variable
const apiKey = env('API_KEY')
const port = env('PORT', 3000) // with default
// Environment checks
if (isProduction()) {
// Production-only code
}
if (isDevelopment()) {
// Development-only code
}
// Custom environment check
if (env('APP_ENV') === 'staging') {
// Staging-only code
}
Configuration Caching
Stacks does not ship a config cache command today. The only config-related CLI applies config-shape codemods when you upgrade:
# Apply config-shape codemods for the latest Stacks version
buddy config:migrate
Validation
Stacks validates your configuration at startup:
There is no config/validation.ts and no rules file. Validation is two
functions, called where booting should stop:
import { requireEnv, validateEnv } from '@stacksjs/env'
// Throws, naming every missing key at once rather than the first one.
requireEnv(['APP_KEY', 'DB_CONNECTION'])
// Checks declared enum values (APP_ENV, DB_CONNECTION, ...) and returns the
// problems rather than throwing, so a caller can decide what is fatal.
const errors = validateEnv()
if (errors.length > 0)
throw new Error(errors.join('\n'))
Beyond that, a config file is TypeScript: defineApp, defineAuth,
defineCache and the rest type it, so a wrong key is a compile error rather
than a startup one.
buddy env:check # validate the current .env
Best Practices
- Never commit secrets - Use
.envfor sensitive values - Use environment variables - Keep config files environment-agnostic
- Validate configuration - Catch errors early at startup
- Type your config - Use TypeScript for autocompletion and safety
Related
- Environment Setup - Setting up your environment
- Cloud Deployment - Cloud configuration
- Database - Database setup