| name | stacks-server |
| description | Use when working with the Stacks development or production server โ server configuration, server middleware, or server startup. Covers @stacksjs/server and storage/framework/server/. |
| license | MIT |
| compatibility | Bun >= 1.3.0, TypeScript |
| allowed-tools | Read Edit Write Bash Grep Glob |
Stacks Server
Key Paths
- Core package:
storage/framework/core/server/src/ (published as @stacksjs/server)
- Server runtime:
storage/framework/server/ (the actual Bun HTTP server + Docker build)
- Server types:
storage/framework/core/types/src/server.ts
- Ports types:
storage/framework/core/types/src/ports.ts
- Package (core):
storage/framework/core/server/package.json
- Package (runtime):
storage/framework/server/package.json
Source Files
core/server/src/
โโโ index.ts # Re-exports: config, controllers/base, imports, maintenance
โโโ config.ts # config() factory โ maps ServerOptions.type to host/port
โโโ config-production.ts # Minimal env-based config for compiled production binaries
โโโ imports.ts # Auto-import system: scan models/jobs/controllers, register bun plugin
โโโ maintenance.ts # Laravel-like maintenance mode (down/up/bypass)
โโโ controllers/
โโโ base.ts # Base Controller class with json/success/error helpers
server/
โโโ src/
โ โโโ index.ts # Bun.serve() entry point โ HTTP + WebSocket + job worker
โ โโโ utils.ts # Docker build helpers (cleanCopy, buildDockerImage, useCustomOrDefaultServerConfig)
โโโ build.ts # Full build pipeline: bundle server + app, post-process, optionally build Docker
โโโ dev # Shell script for local/remote Docker dev (mounts volumes)
โโโ Dockerfile # Multi-stage Bun Docker image (oven/bun:1.3.10)
โโโ package.json # stacks-server v0.70.23
โโโ tsconfig.json # Extends core tsconfig
โโโ tsconfig.docker.json # Docker-specific tsconfig with path aliases
โโโ .dockerignore # Excludes Dockerfile, .git, node_modules, etc.
โโโ .gitignore # Excludes app/, config/, dist/, storage/ (build artifacts)
Server Entry Point (server/src/index.ts)
The runtime server uses Bun.serve() directly. It handles two modes based on environment variables:
HTTP Server Mode (default)
import type { Server, ServerWebSocket } from 'bun'
const server = Bun.serve({
port: Number(process.env.PORT) || 3000,
development: process.env.APP_ENV?.toLowerCase() !== 'production',
async fetch(request: Request, server: Server<any>): Promise<Response | undefined> {
if (server.upgrade(request)) return
return serverResponse(request)
},
websocket: {
open(_ws: ServerWebSocket): void {},
message(_ws: ServerWebSocket, _message: string): void {},
close(_ws: ServerWebSocket, _code: number, _reason?: string): void {},
},
})
Key details:
serverResponse() is imported from @stacksjs/router and handles all route matching, middleware, and response generation
- WebSocket upgrade is attempted before HTTP routing
development mode is enabled when APP_ENV is not production or prod
- SIGINT handler gracefully exits the process
Queue Worker Mode
When QUEUE_WORKER env var is set, the same entry point runs a single job instead of starting the HTTP server:
if (process.env.QUEUE_WORKER) {
const jobName = process.env.JOB.replace(/\.ts$/, '').replace(/[^a-zA-Z0-9_-]/g, '')
const jobModule = await import(`./app/Jobs/${jobName}`)
await retry(() => jobModule.default.handle(), {
backoffFactor: Number(process.env.JOB_BACKOFF_FACTOR) || 2,
retries: Number(process.env.JOB_RETRIES) || 3,
initialDelay: Number(process.env.JOB_INITIAL_DELAY) || 1000,
jitter: process.env.JOB_JITTER === 'true',
})
process.exit(0)
}
Environment variables for job execution:
QUEUE_WORKER -- enables worker mode (any truthy value)
JOB -- job file name (e.g., SendEmail.ts), sanitized to prevent path traversal
JOB_RETRIES -- retry count (default: 3)
JOB_BACKOFF_FACTOR -- exponential backoff multiplier (default: 2)
JOB_INITIAL_DELAY -- initial retry delay in ms (default: 1000)
JOB_JITTER -- enable jitter on retries ('true' to enable)
Server Config (core/server/src/config.ts)
function config(options: ServerOptions): { host: string, port: number, open: boolean }
Maps a ServerOptions.type to a host/port pair using the ports config object. Supported types:
| Type | Port Source |
|---|
frontend | ports.frontend |
backend | ports.backend |
api | ports.api |
admin | ports.admin |
library | ports.library |
desktop | ports.desktop |
docs | ports.docs |
email | ports.email |
inspect | ports.inspect |
system-tray | ports.systemTray |
database | ports.database |
All types resolve to host: 'localhost'. If no type is provided or it doesn't match, falls back to host: options.host || 'stacks.localhost' and port: options.port || 3000.
ServerOptions Type
interface ServerOptions {
type?: 'frontend' | 'backend' | 'api' | 'library' | 'desktop'
| 'docs' | 'email' | 'admin' | 'system-tray' | 'database'
host?: string
port?: number
open?: boolean
}
Ports Interface
interface Ports {
frontend: number
backend: number
admin: number
library: number
desktop: number
email: number
docs: number
inspect: number
api: number
systemTray: number
database: number
}
Production Config (core/server/src/config-production.ts)
Minimal config for compiled binaries -- no runtime file loading, all env-based:
const config = {
app: {
name: process.env.APP_NAME || 'Stacks',
env: process.env.APP_ENV || 'production',
debug: process.env.APP_DEBUG === 'true' || false,
url: process.env.APP_URL || 'https://stacksjs.com',
},
server: {
port: Number(process.env.PORT) || 3000,
host: '0.0.0.0',
},
logging: {
level: process.env.LOG_LEVEL || 'info',
},
}
Used by start.ts (the production binary entry point) with SKIP_CONFIG_LOADING=true to bypass dynamic config imports.
Production Start (core/server/src/start.ts)
Entry point for compiled production binaries. Sequence:
- Sets
__STACKS_BINARY_MODE__ = true on globalThis (prevents auto-registration in routes)
- Sets
SKIP_CONFIG_LOADING = 'true' env var
- Imports
loadRoutes and serve from @stacksjs/router
- Loads routes from the
app/Routes.ts registry via loadRoutes(routeRegistry)
- Loads ORM auto-generated routes from
../../orm/routes (model CRUD endpoints)
- Calls
serve({ port, host }) to start the Bun HTTP server
loadRoutes(routeRegistry)
.then(async () => {
await import('../../orm/routes')
serve({ port: config.server.port, host: config.server.host })
})
Auto-Imports System (core/server/src/imports.ts)
initiateImports()
function initiateImports(): void
Registers a Bun bundler plugin (bun-plugin-auto-imports) that makes models, jobs, controllers, and resource functions available globally without explicit imports.
Scan order (user overrides framework overrides defaults):
- Models:
app/Models/ > storage/framework/defaults/app/Models/ > storage/framework/defaults/app/Models/
- Jobs:
app/Jobs/
- Controllers:
app/Controllers/ > storage/framework/defaults/app/Controllers/
- Functions:
resources/functions/
Outputs:
.d.ts file: storage/framework/types/server-auto-imports.d.ts
- ESLint config:
storage/framework/server-auto-imports.json
- Runtime index files:
storage/framework/auto-imports/ (see below)
generateAutoImportFiles()
async function generateAutoImportFiles(): Promise<void>
Generates runtime-importable index files under storage/framework/auto-imports/:
functions.ts -- re-exports from resources/functions/
models.ts -- re-exports default exports from model definition files as named exports
jobs.ts -- re-exports default exports from job definition files
controllers.ts -- re-exports default exports from controller definition files
index.ts -- combined re-export of all above
globals.ts -- script that assigns all exports to globalThis
injectGlobalAutoImports()
async function injectGlobalAutoImports(): Promise<void>
Dynamically imports storage/framework/auto-imports/index.ts and assigns all exports to globalThis. Call this early in application startup to make models available globally (e.g., Post.where('title', 'test') without imports).
scanDefineModelExports(dir: string): ExportInfo[]
Internal helper that scans a directory for .ts files (excluding .d.ts, index.ts, README*) and returns file names as export info. Used for models, jobs, and controllers that use export default defineModel(...) pattern.
Base Controller (core/server/src/controllers/base.ts)
class Controller {
protected json(data: any, status?: number): ResponseData
protected success(data: any): ResponseData
protected created(data: any): ResponseData
protected noContent(): any
protected error(message: string, status?: number): ResponseData
protected notFound(message?: string): ResponseData
protected unauthorized(message?: string): ResponseData
protected forbidden(message?: string): ResponseData
protected validate(request: Request, rules: Record<string, any>): Promise<void>
}
Usage:
import { Controller } from '@stacksjs/server'
class UserController extends Controller {
async index() {
const users = await User.all()
return this.success(users)
}
async store(request: Request) {
await this.validate(request, { name: 'required', email: 'required|email' })
const user = await User.create(request.body)
return this.created(user)
}
async show(id: number) {
const user = await User.find(id)
if (!user) return this.notFound('User not found')
return this.success(user)
}
}
Maintenance Mode (core/server/src/maintenance.ts)
Laravel-like maintenance mode that writes/removes a storage/framework/down file.
Core Functions
async function down(options?: Partial<MaintenancePayload>): Promise<void>
async function up(): Promise<void>
async function isDownForMaintenance(): Promise<boolean>
async function maintenancePayload(): Promise<MaintenancePayload | null>
MaintenancePayload
interface MaintenancePayload {
time: number
message?: string
retry?: number
secret?: string
allowed?: string[]
status?: number
template?: string
redirect?: string
}
Bypass Helpers
function isAllowedIp(ip: string, allowed?: string[]): boolean
function hasValidBypassCookie(cookies: Record<string, string>, secret: string): boolean
function isSecretPath(path: string, secret: string): boolean
function bypassCookieValue(secret: string): string
Response Helpers
function maintenanceHtml(payload: MaintenancePayload): string
function maintenanceResponse(payload: MaintenancePayload): Response
Usage
import { down, up, isDownForMaintenance } from '@stacksjs/server'
await down({ secret: 'my-bypass-token', retry: 300 })
if (await isDownForMaintenance()) {
const payload = await maintenancePayload()
}
await up()
Docker Build Pipeline (server/build.ts)
The build process (bun build.ts from storage/framework/server/):
- Stop existing container: Checks for running
stacks-server Docker container and stops it
- Clean previous build: Deletes
app/, config/, dist/, docs/, storage/ from server dir
- Bundle server:
Bun.build() with entrypoints: ['./src/index.ts'], output to ./dist, ESM format, target: 'bun'
- Bundle app: Scans all
*.ts and *.js files under app/, builds to server/app/, splitting disabled, @swc/wasm externalized
- Post-process app: Rewrites
storage/framework/server references to dist in output JS files
- Post-process dist: Strips
export { ENV_KEY, ENV_SECRET, fromEnv }; from bundled output (workaround for bundler issue)
- Build Docker image (conditional): Only if
cloud.api?.deploy is truthy
Docker Build (server/src/utils.ts)
async function buildDockerImage(): Promise<void>
async function useCustomOrDefaultServerConfig(): Promise<void>
async function cleanCopy(sourcePath: string, targetPath: string): Promise<void>
buildDockerImage():
- Cleans old CDK artifacts (
cdk.out/, cdk.context.json, dist.zip)
- Removes
.DS_Store, sourcemaps, cache files
- Copies
config/, docs/, storage/, .env into server/
- Optimizes: strips
node_modules, src/, types/, cloud/cdk_out
- Runs
docker build --pull -t <app-slug> .
useCustomOrDefaultServerConfig(): If a server/ directory exists at project root, copies it into the framework server directory, otherwise uses defaults.
Dockerfile
Multi-stage build based on oven/bun:1.3.10:
# Builder stage
FROM oven/bun:1.3.10 AS builder
WORKDIR /usr/src
COPY ./app ./config ./docs ./dist ./tsconfig.docker.json ./
# Release stage
FROM oven/bun:1.3.10 AS release
WORKDIR /usr/src
# Copies app, config, docs, dist, tsconfig from builder
# Sets up /usr/src/storage as a volume
# Installs curl for healthcheck
# Runs as non-root 'bun' user
EXPOSE 3000/tcp
ENTRYPOINT ["bun", "run", "dist/index.js"]
dev Script (server/dev)
Shell script for running the Docker container locally or against remote storage (EFS):
./dev local
./dev
Environment Variables
| Variable | Default | Used In |
|---|
PORT | 3000 | server/src/index.ts, config-production.ts |
APP_ENV | 'production' | config-production.ts, server/src/index.ts |
APP_NAME | 'Stacks' | config-production.ts |
APP_URL | 'https://stacksjs.com' | config-production.ts |
APP_DEBUG | false | config-production.ts |
LOG_LEVEL | 'info' | config-production.ts |
SKIP_CONFIG_LOADING | unset | start.ts (set to 'true') |
QUEUE_WORKER | unset | server/src/index.ts |
JOB | required if worker | server/src/index.ts |
JOB_RETRIES | 3 | server/src/index.ts |
JOB_BACKOFF_FACTOR | 2 | server/src/index.ts |
JOB_INITIAL_DELAY | 1000 | server/src/index.ts |
JOB_JITTER | 'false' | server/src/index.ts |
CLI Commands
buddy dev or bun run dev -- start development server (uses storage/framework/server/src/index.ts with hot reload)
buddy serve or bun run serve -- start production server
buddy build:server -- run the Docker build pipeline (storage/framework/server/build.ts)
buddy down -- put app in maintenance mode
buddy up -- bring app out of maintenance mode
Gotchas
- The server lives in TWO places:
storage/framework/core/server/ is the published @stacksjs/server package (config, controllers, imports, maintenance); storage/framework/server/ is the actual runtime Bun HTTP server and Docker build infrastructure
- The server runtime (
storage/framework/server/) has its own package.json, tsconfig.json, and is excluded from workspaces -- it is a standalone deployable unit
- The
.gitignore in storage/framework/server/ excludes app/, config/, dist/, storage/ because these are populated at build time by build.ts
start.ts sets __STACKS_BINARY_MODE__ = true on globalThis -- this flag prevents route auto-registration and is only used for compiled production binaries
- Port configuration comes from
@stacksjs/config (ports export), not from a standalone config/ports.ts file -- the Ports interface is defined in storage/framework/core/types/src/ports.ts
- The
config() function always sets open: false (browser auto-open is commented out)
- The default host for unnamed server types is
'stacks.localhost', not 'localhost'
- Production config binds to
0.0.0.0 (all interfaces), while development config uses localhost
serverResponse() from @stacksjs/router is the single function that handles ALL HTTP request routing -- the server itself has no routing logic
- ORM auto-routes are loaded AFTER manual routes in
start.ts so that routeExists() correctly detects conflicts
- The queue worker mode reuses the same entry point (
server/src/index.ts) -- when QUEUE_WORKER is set, no HTTP server starts; it runs the job and exits
- Job names are sanitized with
/[^a-zA-Z0-9_-]/g to prevent path traversal attacks
generateAutoImportFiles() runs fire-and-forget during initiateImports() -- errors are logged but do not block server startup
- User models/controllers/jobs take priority over framework defaults due to deduplication order (user dirs are scanned first)
- The Docker image runs as non-root user
bun with /usr/src/storage as a persistent volume
build.ts strips export { ENV_KEY, ENV_SECRET, fromEnv } from bundled output as a workaround for a Bun bundler issue
tsconfig.docker.json has path aliases that remap @stacksjs/* to core/*/dist -- this is critical for the Docker build to resolve packages without workspace symlinks