- 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() HTTP entry and one-shot queue 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 package
├── 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 HTTP runtime delegates fetch requests to serverResponse from
`@stacksjs/router/runtime`. It has no WebSocket upgrade handlers. Realtime
clients connect to the separate authenticated broadcast server described by
`stacks-realtime`.
PORT defaults to 3000; APP_ENV production/prod disables development mode.
Production enables reusePort for replacement instances on supported platforms.
SIGTERM stops accepting connections and drains in-flight requests, with a
SHUTDOWN_GRACE_MS deadline (default fifteen seconds). Supervisors still own
restart and deploy health-gating policy.
With QUEUE_WORKER set, this entry instead loads the sanitized JOB file and runs
its handle with retry. JOB_RETRIES, JOB_BACKOFF_FACTOR, JOB_INITIAL_DELAY and
JOB_JITTER configure that one-shot mode. It is distinct from the durable
`buddy queue:work` processor.
## Server Config (core/server/src/config.ts)
```typescript
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
```typescript
interface ServerOptions {
type?: 'frontend' | 'backend' | 'api' | 'library' | 'desktop'
| 'docs' | 'email' | 'admin' | 'system-tray' | 'database'
host?: string
port?: number
open?: boolean
}
```
### Ports Interface
```typescript
interface Ports {
frontend: number
backend: number // proxies api
admin: number
library: number
desktop: number
email: number
docs: number
inspect: number
api: number // the bun server
systemTray: number
database: number // i.e. DynamoDB local
}
```
## Production Config (core/server/src/config-production.ts)
Minimal config for compiled binaries -- no runtime file loading, all env-based:
```typescript
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)
The compiled-binary entry marks binary mode and disables external runtime config
loading. It reads appRouteRegistry at runtime, loads manual routes first, then
tries `@stacksjs/orm/routes` with a checkout-relative fallback. Missing ORM
routes warn; unresolved registered route middleware aborts startup rather than
serving an unguarded route. Bun receives `hostname`, not a host option.
Unhandled rejections reach the shared report hook; uncaught exceptions report,
flush logging and exit so a supervisor can restart. This entry's startup contract
differs from the Docker HTTP entry and the STX production views server; inspect
the selected build/deploy entrypoint before copying environment assumptions.
## Auto-Imports System (core/server/src/imports.ts)
### initiateImports()
```typescript
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`
- Compatibility manifest: storage/framework/server-auto-imports.json; lint remains pickier
- Runtime index files: `storage/framework/auto-imports/` (see below)
### generateAutoImportFiles()
```typescript
async function generateAutoImportFiles(): Promise<void>
```
Package-based apps address framework defaults through `@stacksjs/defaults` exports, preserving application overrides. Generated imports never depend on the physical npm or Pantry installation path. An app created with `--with-core` keeps source-relative defaults imports; detection uses the framework source layout, independently of the application's package name.
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()
```typescript
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)
```typescript
class Controller {
protected json(data: unknown, status?: ResponseStatus): Response // default status: 200
protected success(data: unknown): Response // alias for json(data, 200)
protected created(data: unknown): Response // json(data, 201)
protected noContent(): any // response.noContent()
protected error(message: string, status?: ResponseStatus): Response // json({ error }, 500)
protected notFound(message?: string): Response // error(msg, 404)
protected unauthorized(message?: string): Response // error(msg, 401)
protected forbidden(message?: string): Response // error(msg, 403)
protected validate(request: Request, rules: Record<string, unknown>): Promise<void>
}
```
Usage:
```typescript
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
```typescript
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
```typescript
interface MaintenancePayload {
time: number // When maintenance was activated (Date.now())
message?: string // Display message (default: 'We are currently performing maintenance...')
retry?: number // Retry-After header value in seconds
secret?: string // Bypass token
allowed?: string[] // Allowed IP addresses
status?: number // HTTP status (default: 503)
template?: string // Custom template path
redirect?: string // Redirect URL instead of showing maintenance page
}
```
### Bypass Helpers
```typescript
function isAllowedIp(ip: string, allowed?: string[]): boolean
// Localhost IPs ('127.0.0.1', '::1', 'localhost') are always allowed
function hasValidBypassCookie(cookies: Record<string, string>, secret: string): boolean
// Checks for cookie: stacks_maintenance_bypass=<secret>
function isSecretPath(path: string, secret: string): boolean
// Matches /<secret> or /<secret>/...
function bypassCookieValue(secret: string): string
// Returns: 'stacks_maintenance_bypass=<secret>; Path=/; HttpOnly; SameSite=Lax'
```
### Response Helpers
```typescript
function maintenanceHtml(payload: MaintenancePayload): string
// Returns a styled HTML page with gradient background
function maintenanceResponse(payload: MaintenancePayload): Response
// Returns Response with correct status, Retry-After header, or 302 redirect
```
### Usage
```typescript
import { down, up, isDownForMaintenance } from '@stacksjs/server'
// Put app in maintenance mode with bypass secret
await down({ secret: 'my-bypass-token', retry: 300 })
// Check if in maintenance
if (await isDownForMaintenance()) {
const payload = await maintenancePayload()
// ...
}
// Bring app back up
await up()
```
## Sitemap and robots.txt (core/actions/src/seo.ts)
The views server answers `/sitemap.xml` and `/robots.txt` for every app, generated from
`resources/views` at request time. `buddy dev` (`core/actions/src/dev/views.ts`) and `buddy serve`
(`core/buddy/src/production-server.ts`) call the same `createSiteSeoHandler()` at the same point in
`onRequest` (after redirects and rewrites), so dev and production answer identically. No route, no
build step, no app code. Do not add `route.get('/sitemap.xml')` to `routes/`: the views server does
not forward root GETs to the API.
- **Listed**: every static route under `resources/views`, with `lastmod` from the file, plus the
blog's published posts (`content/blog`, or a CMS-built `dist/blog/sitemap.xml`) merged into the
same file. `/blog/sitemap.xml` is still served too.
- **Not listed**: dynamic routes (`[slug].stx`), error and holding pages (`404`, `errors/*`,
`coming-soon`), `components/`, `layouts/`, `partials/`, `emails/`, `_`-prefixed files, pages behind
`definePageMeta({ middleware: ['auth'] })`, paths in `seo.sitemap.exclude`, paths robots.txt
disallows, and any page that says **noindex**. The framework's default views (`/login`, ...) are
never listed.
- **noindex**, the one signal, read from the page source: `<meta name="robots" content="noindex">`,
`robots: 'noindex'` in a head object, or `const noindex = true` in the server script (the flag a
shared head partial turns into the tag - the scan cannot follow an `@include`, so spell it that
way). `definePageMeta({ sitemap: false })` also keeps a page out.
- **robots.txt**: `User-agent: *`, `Allow: /`, `Disallow: /api/` plus `seo.robots.disallow`, extra
groups from `seo.robots.rules`, and `Sitemap: <origin>/sitemap.xml`. Never disallow a page just to
hide it: a crawler that cannot fetch it never sees its noindex.
Voir sur GitHub