Tri-target deployment design: Vercel (Node.js), Vercel (Bun experimental), and Koyeb (Bun container).
Status: only the Vercel Node.js target is wired. The
sveltekit()options invite.config.tshardcodeadapter-vercel;DEPLOY_TARGET,svelte-adapter-bun, and the Dockerfile referenced below do not exist yet. The Bun/Koyeb sections are design intent for the planned container target, kept because the job system and env parity were built to support them.
Strategy: Same codebase, different configurations. See stack/ops/deployment.md for decision rationale and current status.
Quick Start
# Vercel (Node.js) — Stable, the only target wired today. Never deployed by hand:
# Stas says "ship it" → squash into GitHub main → GitLab sync → Vercel builds GitLab main
bun run build # the same build runs locally/in the cloud as `bun run validate:build`
# Vercel (Bun) — PLANNED, experimental
bun run build
# Add "bunVersion": "1.x" to vercel.json
# Koyeb (Bun) — PLANNED; DEPLOY_TARGET and the Dockerfile do not exist yet
DEPLOY_TARGET=bun bun run build
# Deploy via Dockerfile
Adapter Configuration
Dynamic Adapter Selection (planned)
Today vite.config.ts imports @sveltejs/adapter-vercel unconditionally and passes it to sveltekit({ adapter }). The switch below is the shape the container target will introduce:
// vite.config.ts
import adapterVercel from '@sveltejs/adapter-vercel';
import { sveltekit } from '@sveltejs/kit/vite';
import adapterBun from 'svelte-adapter-bun';
const target = process.env.DEPLOY_TARGET;
const adapter = target === 'bun'
? adapterBun({ precompress: true })
: adapterVercel({ runtime: 'nodejs22.x', regions: ['fra1'] });
export default defineConfig({
plugins: [sveltekit({ adapter /* , version, csp, preprocess */ })],
});
Adapter Dependencies
Dev dependencies (svelte-adapter-bun is not installed until the container target lands, and must support SvelteKit 3 first):
"@sveltejs/adapter-vercel": "^7.0.1",
"svelte-adapter-bun": "^0.5.x"
See development-environment.md for installation workflow.
Package.json Scripts
{
"scripts": {
"build": "vite build",
"build:vercel": "vite build",
"build:bun": "DEPLOY_TARGET=bun vite build",
"preview": "vite preview",
"preview:bun": "DEPLOY_TARGET=bun bun run build && bun ./build/index.js"
}
}
Vercel Deployment (Node.js)
Setup
- Git source: the GitLab mirror
gitlab.com/ApeDevil/v10r, production branchmain. GitHub is the development authority;gitlab-sync.ymlcopies itsmainthere (shipping.md). - Framework preset: SvelteKit (auto-detected)
- Build command:
bun run build(default) - Output directory:
.vercel/output(auto-configured) - Node.js version: 22.x — the engine the gate runs on (Runtime parity)
Function region
The functions run in fra1, declared as the adapter default in vite.config.ts
(sveltekit({ adapter: adapter({ runtime: 'nodejs22.x', regions: ['fra1'] }) })) — next to Neon in eu-central-1, so
the two blocking database hops of a request stay in one region. Route-level config exports
(maxDuration on the AI routes) merge over the defaults and keep the region. Before the
declaration the deployment ran on Vercel's default iad1 (measured 2026-09-13 from
x-vercel-id: fra1::iad1::…); x-vercel-id reads edge::function::id and is the check.
vercel.json
vercel.json holds three things: the git.deploymentEnabled guard
("**": false, "main": true — only main builds), the no-cache header for
/service-worker.js, and the cron schedules. Read the file; it is short and is the only copy.
Vercel cron calls are authenticated with CRON_SECRET. See Scheduled Jobs below.
Environment Variables
The model. src/env.ts declares every variable the app reads, all optional at the schema
level; each reader decides what unset means (fail at import, degrade a feature, or apply a code
default). .env.example lists them in three tiers — required, feature switches, and commented
overrides whose defaults are right. A variable is set by hand only when it is a secret, differs
per deployment, or is an account id no code can know. Everything else is a code default, derived,
or supplied by the platform. Since every deployment is a production deployment
(below), each value is set for Production (Preview may mirror it
for a manual vercel deploy).
Set by hand in Vercel → Settings → Environment Variables:
| Variable | Notes |
|---|---|
NEON_DATABASE_URL_PROD |
Neon connection string (direct, non-pooled). Neon's Vercel integration injects DATABASE_URL instead — nothing reads that |
BETTER_AUTH_SECRET |
≥32 chars. Root of every derived key (security/subkey.ts) |
BETTER_AUTH_URL |
The canonical site URL, https://www.v10r.dev. Required outside dev; auth, passkeys, the Discord redirect URI and the Web Push contact derive from it (auth/site-url.ts). Not derivable: VERCEL_PROJECT_PRODUCTION_URL is the apex, which redirects |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET, GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
OAuth apps; required at import |
ENCRYPTION_KEY |
64-char hex (openssl rand -hex 32). Seals every stored credential — the AI provider keys (Admin → AI → Models) and name-check vendor secrets (Admin → Name check). Placeholders are rejected at boot; losing it invalidates all of them. Those credentials themselves are not environment variables |
ADMIN_USER_ID |
Comma-separated admin user ids |
ALTCHA_HMAC_KEY |
≥32 chars; the build fails without it |
UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN |
Rate limiting and the captcha replay store. Without them the limiter fails closed in production |
CRON_SECRET |
Vercel sends it as the bearer token on every cron call (Scheduled Jobs) |
Feature blocks of .env.example §2 |
Neo4j, R2, Resend, Telegram, Discord (client id/secret, bot token), VAPID keypair, MCP tokens and salt, Neon Management API, DB monitoring, External Service Lab, PRIVACY_CONTACT_EMAIL. Each block is all-or-nothing; unset, that feature is off |
Supplied by Vercel, never set by hand: VERCEL (disables the in-process scheduler —
platform/index.ts), VERCEL_ENV (preview-only passkey switch, robots, MCP telemetry lane),
VERCEL_GIT_COMMIT_SHA (build version, MCP server info), NODE_ENV=production. They require
"Automatically expose System Environment Variables", which is on by default.
Not set on Vercel because the code default is right: BOT_DETECTION_MODE (live, the only
mode a build accepts), VAPID_SUBJECT (the https site URL), NEON_PARENT_BRANCH_ID (the dev branch's parent), DBOPS_AUTO_REFRESH_ENABLED (off), the
subkey overrides. Meaningless on Vercel: JOB_INTERVAL_MS, DELIVERY_INTERVAL_MS,
JOBS_DEV_ENABLED, ANALYTICS_DEV_TRACKING (container or dev only). Only while rotating:
ENCRYPTION_KEY_PREVIOUS (Rotation).
The runtime-lab service on Render needs one variable, SERVICE_TOKEN; Render injects PORT
and the service binds 0.0.0.0 with a default of 8080 for local runs
(external-service.md). The GitLab sync workflow needs one
GitHub secret, GITLAB_PROMOTION_TOKEN, in environment gitlab-promotion.
Adapter Options
// vite.config.ts — inside sveltekit({ ... })
import adapter from '@sveltejs/adapter-vercel';
sveltekit({
adapter: adapter({
// Deploy to edge (faster, limited Node.js APIs)
runtime: 'edge',
// Or use Node.js runtime (full API support)
runtime: 'nodejs22.x',
// Split routes into separate functions
split: true,
// Memory limit (MB)
memory: 1024,
// Timeout (seconds)
maxDuration: 10,
}),
});
No preview deployments
Feature branches never deploy: the vercel.json guard switches the git integration off for
every branch but main, and nothing is pushed to GitLab except main. A feature is seen on
its branch (cloud or local session), then shipped. Every deployment in the project is a
production deployment. The code still treats VERCEL_ENV === 'preview' defensively, for a
manual vercel deploy.
Vercel Deployment (Bun) — Experimental
Warning: Vercel's Bun runtime officially supports Next.js, Express, Hono, and Nitro. SvelteKit is not officially listed but may work. Test thoroughly before production use.
Why Try This?
- 28% lower latency compared to Node.js (Vercel benchmarks)
- Same Vercel DX (dashboard, analytics)
- One config line to enable
- Easy to revert if issues arise
Setup
Same as Node.js deployment, plus one addition to vercel.json:
{
"framework": "sveltekit",
"bunVersion": "1.x",
"crons": [
{ "path": "/api/cron/due", "schedule": "0 3 * * *" },
{ "path": "/api/cron/bot-ranges-refresh", "schedule": "45 4 * * *" }
]
}
The key addition is "bunVersion": "1.x" which enables Bun runtime for all functions.
What Changes
| Aspect | Node.js | Bun |
|---|---|---|
| Runtime | Node.js 22 | Bun 1.x |
| Cold starts | ~200-500ms | ~150-350ms (estimated) |
| Adapter | adapter-vercel |
adapter-vercel (same) |
| Build | bun run build |
bun run build (same) |
| Config | Default | Add bunVersion |
Testing Strategy
- Trial on a separate Vercel project first — v10r has no preview deployments, and production is the only target
- Test all routes — Server functions, API endpoints, load functions
- Check edge cases — File uploads, WebSockets, streaming
- Monitor errors — Watch Vercel logs for Bun-specific issues
- Compare performance — Measure latency vs Node.js deployment
Reverting to Node.js
If issues arise, simply remove the bunVersion line:
{
"framework": "sveltekit"
}
Redeploy and you're back on Node.js.
Known Limitations
| Feature | Status |
|---|---|
| SvelteKit SSR | Should work (uses adapter-vercel) |
| API routes | Should work |
| Edge runtime | Unknown (test needed) |
| Streaming | Should work |
| WebSockets | Unknown (test needed) |
| Native Node modules | May have issues |
Reporting Issues
If SvelteKit + Bun works well, consider:
- Sharing results in Vercel Community
- Opening issue to request official SvelteKit support
Koyeb Deployment (Bun)
Dockerfile
# syntax=docker/dockerfile:1
# ============================================
# Base image
# ============================================
FROM oven/bun:1-alpine AS base
WORKDIR /app
# ============================================
# Install dependencies
# ============================================
FROM base AS deps
# Copy package files
COPY package.json bun.lock ./
# Install all dependencies (including devDependencies for build)
RUN bun install --frozen-lockfile
# ============================================
# Build stage
# ============================================
FROM base AS builder
# Copy dependencies
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Set build-time environment
ENV NODE_ENV=production
ENV DEPLOY_TARGET=bun
# Build the application
RUN bun run build
# ============================================
# Production dependencies only
# ============================================
FROM base AS prod-deps
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
# ============================================
# Production image
# ============================================
FROM base AS runner
# Security: run as non-root user
USER bun
# Copy production dependencies
COPY --from=prod-deps --chown=bun:bun /app/node_modules ./node_modules
# Copy built application
COPY --from=builder --chown=bun:bun /app/build ./build
COPY --from=builder --chown=bun:bun /app/package.json ./
# Environment variables for reverse proxy
ENV NODE_ENV=production
ENV PROTOCOL_HEADER=x-forwarded-proto
ENV HOST_HEADER=x-forwarded-host
ENV PORT=3000
EXPOSE 3000
# Start the application
CMD ["bun", "run", "./build/index.js"]
.dockerignore
node_modules
.svelte-kit
build
.env*
.git
.gitignore
*.md
Dockerfile
.dockerignore
Koyeb Setup
-
Sign up at koyeb.com (no credit card required)
-
Create app → Select GitHub repository
-
Builder: Dockerfile
-
Instance type: Nano (free tier)
- 512 MB RAM
- 0.1 vCPU
- Frankfurt or Washington D.C.
-
Environment variables: Add in Koyeb dashboard
-
Port: 3000
Environment Variables
Set in Koyeb dashboard → Service → Settings → Environment:
| Variable | Required | Notes |
|---|---|---|
NEON_DATABASE_URL_PROD |
Yes | Neon connection string |
UPSTASH_REDIS_REST_URL |
Yes | Upstash Redis REST URL — enables rate limiting. Without it the limiter passes through in dev and fails closed in prod |
UPSTASH_REDIS_REST_TOKEN |
Yes | Upstash Redis REST token (paired with the URL above) |
ORIGIN |
Yes | https://your-app.koyeb.app |
R2_ACCOUNT_ID / R2_BUCKET_NAME |
If using R2 | R2 account and bucket (the endpoint is derived from the account id) |
R2_ACCESS_KEY_ID |
If using R2 | R2 credentials |
R2_SECRET_ACCESS_KEY |
If using R2 | R2 credentials |
Adapter Options
// vite.config.ts — inside sveltekit({ ... })
import adapter from 'svelte-adapter-bun';
sveltekit({
adapter: adapter({
// Enable gzip/brotli compression
precompress: true,
// Development mode logging
development: false,
// Dynamic imports for routes
dynamic_origin: true,
// WebSocket support (experimental)
websocket: false,
}),
});
Local Testing
Test the Docker build locally:
# Build image
podman build -t velociraptor .
# Run container
podman run -p 3000:3000 \
-e NEON_DATABASE_URL_PROD="postgresql://..." \
-e ORIGIN="http://localhost:3000" \
velociraptor
# Open http://localhost:3000
Scheduled Jobs
Recurring background work runs differently per platform. The same job registry serves both.
How it works
| Platform | Trigger | Entry point |
|---|---|---|
| Vercel | HTTP cron via vercel.json |
GET /api/cron/due once a day (everything due, in registry order); GET /api/cron/[job] for standalone jobs |
| Container | setInterval in hooks.server.ts |
Direct function call |
Platform detection — src/lib/server/platform/index.ts reads VERCEL, FLY_APP_NAME and RAILWAY_ENVIRONMENT from $app/env/private (declared in src/env.ts). If VERCEL is set, platform.persistent = false and the scheduler is a no-op. FLY_APP_NAME and RAILWAY_ENVIRONMENT map to persistent platforms (fly, railway). Otherwise it falls through to the default { id: 'container', persistent: true } and the scheduler starts — persistence is the default, not a marker variable.
Job registry — src/lib/server/jobs/index.ts maps slugs to execute() functions plus a cadence. Both trigger paths call the same registry; jobsDueOn(date) is what the daily sweep runs.
// src/lib/server/jobs/index.ts
export const jobs: Record<string, Job> = {
'session-cleanup': { execute: sessionCleanup, cadence: 'daily' },
};
Adding a job
- Write a pure function in
src/lib/server/jobs/:
// src/lib/server/jobs/my-job.ts
export async function myJob(): Promise<number> {
// do work, return count of affected rows
return affectedCount;
}
- Register it:
// src/lib/server/jobs/index.ts
import { myJob } from './my-job';
export const jobs: Record<string, Job> = {
'session-cleanup': { execute: sessionCleanup, cadence: 'daily' },
'my-job': { execute: myJob, cadence: 'daily' }, // add this line — position = run order
};
- Nothing else for Vercel: the daily
/api/cron/duesweep picks it up. Only a job that is too slow for the sweep (external fetches measured in tens of seconds) getsstandalone: trueand its own entry:
{
"crons": [
{ "path": "/api/cron/due", "schedule": "0 3 * * *" },
{ "path": "/api/cron/my-slow-job", "schedule": "0 5 * * *" }
]
}
Container mode runs all registered jobs on the same interval (JOB_INTERVAL_MS, default 3 hours). Vercel crons can have independent schedules.
Sub-daily crons need Vercel Pro — Hobby allows daily only. Every cron in
vercel.jsonis daily or weekly to stay within Hobby;dbops-reaperis a daily job inside the/api/cron/duesweep (src/lib/server/jobs/index.ts). See data/neon-branch-refresh.md.
HTTP endpoint
GET /api/cron/[job] requires a bearer token:
Authorization: Bearer <CRON_SECRET>
Vercel sends this automatically when CRON_SECRET is set in the dashboard. The CRON_SECRET value must match across vercel.json config and the dashboard environment variable.
Returns:
{ "success": true, "deleted": 42 }
Returns 401 if the token is missing or wrong. Returns 404 if the job slug is not in the registry.
Scheduler behaviour (container)
- Runs 5 seconds after startup (catches expired sessions immediately)
- Repeats every
JOB_INTERVAL_MSmilliseconds (default:10800000= 3 hours) globalThis.__v10r_schedulerprevents duplicate intervals on HMR restartstimer.unref()lets the process exit cleanly without a pending interval- Clears on
SIGTERMfor graceful shutdown
Environment variables
| Variable | Platform | Notes |
|---|---|---|
CRON_SECRET |
Both | Bearer token for HTTP cron endpoint. Generate with openssl rand -base64 32 |
JOB_INTERVAL_MS |
Container | Interval in ms. Omit to use the 3-hour default |
VERCEL |
Vercel | Set automatically by Vercel. Disables in-process scheduler |
CI/CD Pipeline
Reference only — this project intentionally has no CI (solo dev; validate locally with
bun run validate, see CLAUDE.md "Verification"). There is no.gitlab-ci.yml; the one GitHub workflow,.github/workflows/gitlab-sync.yml, copies GitHubmainto GitLabmainand tests nothing (shipping.md). The pipelines below are templates for a future multi-target setup, not active config.
GitLab CI (All Targets)
# .gitlab-ci.yml
stages:
- build
- deploy
variables:
BUN_VERSION: "1.1"
# Build for Vercel
build:vercel:
stage: build
image: oven/bun:1
script:
- bun install --frozen-lockfile
- bun run build
artifacts:
paths:
- .vercel/output
only:
- main
# Build for Koyeb
build:koyeb:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
# Deploy to Vercel
deploy:vercel:
stage: deploy
image: node:22-alpine
script:
- npx vercel --prod --token=$VERCEL_TOKEN
needs:
- build:vercel
only:
- main
# Deploy to Koyeb
deploy:koyeb:
stage: deploy
image: curlimages/curl:latest
script:
- |
curl -X POST "https://app.koyeb.com/v1/services/$KOYEB_SERVICE_ID/redeploy" \
-H "Authorization: Bearer $KOYEB_API_TOKEN"
needs:
- build:koyeb
only:
- main
GitHub Actions (All Targets)
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy-vercel:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun run build
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
deploy-koyeb:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push Docker image
run: |
echo "${{ secrets.KOYEB_DOCKER_TOKEN }}" | docker login -u koyeb --password-stdin
docker build -t koyeb.io/${{ secrets.KOYEB_APP }}:${{ github.sha }} .
docker push koyeb.io/${{ secrets.KOYEB_APP }}:${{ github.sha }}
- name: Deploy to Koyeb
run: |
curl -X PATCH "https://app.koyeb.com/v1/services/${{ secrets.KOYEB_SERVICE_ID }}" \
-H "Authorization: Bearer ${{ secrets.KOYEB_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"definition":{"docker":{"image":"koyeb.io/${{ secrets.KOYEB_APP }}:${{ github.sha }}"}}}'
Database Migrations
Vercel
Not used in v10r. The Vercel build is bun run build only; the schema is pushed from a
local session (bun run db:push needs the real database credentials, so a cloud session
cannot run it). Push a schema change before shipping the code that needs it. The shape below
is what a project that wants push-on-deploy would add — it would run on every production build:
{
"buildCommand": "bun run db:push && bun run build"
}
Koyeb
Run migrations before starting the app in Dockerfile:
# Add migration step
CMD ["sh", "-c", "bun run db:push && bun run ./build/index.js"]
Or use a separate migration job:
# Run migration manually
koyeb service exec <service> -- bun run db:push
Health Checks
Illustrative —
/api/healthis not yet built. There is nosrc/routes/api/health/route today; the live platform/quota health surface is the admin monitoring layer (src/lib/server/monitoring/*). To use the Koyeb health check below, create the route first; until then/api/health404s.
Koyeb Health Check
Configure in Koyeb dashboard → Service → Health checks:
| Setting | Value |
|---|---|
| Path | /api/health |
| Port | 3000 |
| Period | 30s |
| Timeout | 5s |
Health Endpoint
// src/routes/api/health/+server.ts
import { db } from '#lib/server/db/index.ts';
import { sql } from 'drizzle-orm';
export async function GET() {
try {
// Check database connection
await db.execute(sql`SELECT 1`);
return Response.json({
status: 'healthy',
timestamp: new Date().toISOString(),
});
} catch (error) {
return Response.json(
{ status: 'unhealthy', error: 'Database connection failed' },
{ status: 503 }
);
}
}
Cold Starts
Vercel
- Edge runtime: ~50ms cold start
- Node.js runtime: ~200-500ms cold start
- Mitigate: Use edge runtime for latency-sensitive routes
Koyeb
- Free tier: Service sleeps after inactivity
- Cold start: 10-30 seconds
- Mitigate: Implement loading states, use keep-alive ping
// Keep-alive cron (external service like cron-job.org)
// Ping /api/health every 5 minutes to prevent sleep
Debugging
Vercel Logs
# View logs (Vercel CLI, local host only; cloud sessions use the Vercel MCP: get_runtime_logs)
vercel logs <deployment-url>
# Stream logs
vercel logs <deployment-url> --follow
Note: locally the Vercel CLI is installed on the host, not in the container.
Koyeb Logs
# Install Koyeb CLI
curl -fsSL https://raw.githubusercontent.com/koyeb/koyeb-cli/master/install.sh | sh
# View logs
koyeb service logs <service-name>
# Stream logs
koyeb service logs <service-name> --follow
Environment Variable Checklist
Vercel's list is Environment Variables above; this table only adds what a planned container target (Koyeb) changes.
| Variable | Vercel | Koyeb (planned) |
|---|---|---|
| Everything in Environment Variables | Dashboard | Dashboard |
ORIGIN |
Not used | Dashboard (svelte-adapter-bun) |
PROTOCOL_HEADER / HOST_HEADER |
Not used | Dockerfile |
JOB_INTERVAL_MS |
Not used | Optional (3 h default) |
VERCEL, VERCEL_ENV, VERCEL_GIT_COMMIT_SHA |
Platform | Absent — container scheduler runs |
File Structure
/
├── Dockerfile # Koyeb container build
├── .dockerignore # Docker build exclusions
├── vercel.json # Vercel configuration (crons)
├── compose.yaml # Local dev (loads .env)
├── vite.config.ts # sveltekit({...}): adapter selection, function region
└── src/
├── hooks.server.ts # Imports scheduler (bare import activates it)
├── lib/server/
│ ├── platform/
│ │ ├── types.ts # PlatformId, PlatformInfo
│ │ └── index.ts # Platform detection (VERCEL / FLY / RAILWAY, else container default)
│ └── jobs/
│ ├── index.ts # Job registry (slug → execute fn)
│ ├── scheduler.ts# In-process scheduler (containers only)
│ └── session-cleanup.ts # Deletes expired sessions
└── routes/api/cron/
└── [job]/
└── +server.ts # HTTP trigger (Vercel crons)
Summary
| Step | Vercel (Node.js) | Vercel (Bun) | Koyeb (Bun) |
|---|---|---|---|
| Adapter | adapter-vercel |
adapter-vercel |
svelte-adapter-bun |
| Build | bun run build |
bun run build |
DEPLOY_TARGET=bun bun run build |
| Config | Default | Add bunVersion: "1.x" |
Dockerfile |
| Deploy | ship → GitLab sync → Vercel builds main |
same as Node.js | Dockerfile → dashboard |
| Status | Live | Planned (experimental) | Planned |
| Logs | vercel logs |
vercel logs |
koyeb service logs |
| Scheduled jobs | HTTP cron via vercel.json |
HTTP cron via vercel.json |
setInterval in process |
Related
- stack/ops/deployment.md - Decision rationale
- abuse/rate-limits.md - Rate limiting configuration
- middleware.md - Request handling
- error-handling.md - Error pages and logging