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_CSP has 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 serve treats itself as production even on a laptop, and HSTS on localhost commits 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

  1. Never commit secrets - Use .env for sensitive values
  2. Use environment variables - Keep config files environment-agnostic
  3. Validate configuration - Catch errors early at startup
  4. Type your config - Use TypeScript for autocompletion and safety