#Middlewares
Alepha provides built-in middleware modules for common server needs.
#Built-in (AlephaServer)
The following are built into AlephaServer and active by default. Configure via atoms or disable globally.
#Compression
Response compression (gzip, brotli, zstd) based on the client's Accept-Encoding header. Active by default for JSON, HTML, JavaScript, CSS, and plain text responses.
Configure via the compressOptions atom:
1import { compressOptions } from "alepha/server";2 3alepha.store.mut(compressOptions, (old) => ({4 ...old,5 disabled: true, // disable compression entirely6}));
| Option | Default | Description |
|---|---|---|
disabled |
false |
Disable compression entirely |
allowedContentTypes |
["application/json", "text/html", "application/javascript", "text/plain", "text/css"] |
Content types eligible for compression |
#Security Headers (Helmet)
HTTP security headers on every response. Active by default.
Configure via the helmetOptions atom:
1import { helmetOptions } from "alepha/server"; 2 3alepha.store.mut(helmetOptions, (old) => ({ 4 ...old, 5 xFrameOptions: "DENY", 6 contentSecurityPolicy: { 7 directives: { 8 defaultSrc: ["'self'"], 9 scriptSrc: ["'self'", "https://cdn.example.com"],10 },11 },12}));
| Option | Default | Description |
|---|---|---|
disabled |
false |
Disable security headers entirely |
isSecure |
- | Force secure context (HSTS) |
strictTransportSecurity |
{ maxAge: 15552000, includeSubDomains: true } |
HSTS configuration |
xFrameOptions |
"SAMEORIGIN" |
X-Frame-Options header |
xXssProtection |
false |
X-XSS-Protection header |
referrerPolicy |
"strict-origin-when-cross-origin" |
Referrer-Policy header |
contentSecurityPolicy |
- | CSP directives |
#Multipart
Multipart form-data parsing for file uploads. Runs for any route with a body schema when the request's content type is multipart/form-data, and handles z.file() and z.stream() parts.
Configure via the multipartOptions atom:
1import { multipartOptions } from "alepha/server";2 3alepha.store.mut(multipartOptions, (old) => ({4 ...old,5 limit: 50_000_000, // 50MB total6 fileLimit: 10_000_000, // 10MB per file7 fileCount: 20,8}));
| Option | Default | Description |
|---|---|---|
limit |
10000000 (10MB) |
Maximum total multipart request size in bytes |
fileLimit |
5000000 (5MB) |
Maximum single file size in bytes |
fileCount |
10 |
Maximum number of files per request |
#Optional Modules
These are registered with alepha.with().
#CORS
Cross-Origin Resource Sharing comes from the AlephaServerCors module. Register it and configure the corsOptions atom for global behavior:
1import { Alepha } from "alepha";2import { AlephaServerCors, corsOptions } from "alepha/server/cors";3 4const alepha = Alepha.create().with(AlephaServerCors);5alepha.store.mut(corsOptions, (o) => ({6 ...o,7 origin: "https://app.example.com",8 credentials: true,9}));
For per-action CORS, attach the $cors middleware instead: use: [$cors({ origin: "..." })].
| Option | Default | Description |
|---|---|---|
origin |
"*" |
Allowed origins. "*" for all, or comma-separated list. |
methods |
["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] |
Allowed HTTP methods |
headers |
["Content-Type", "Authorization"] |
Allowed request headers |
credentials |
false |
Allow credentials (cookies, auth headers) |
maxAge |
- | Preflight cache duration in seconds |
Alepha automatically creates an OPTIONS preflight route for every path when the CORS module is active - including GET-only paths, which browsers preflight as soon as the request carries a non-simple header such as Authorization.
Responses always carry Vary: Origin, since the allowed origin is reflected from the request.
credentials requires an explicit origin. With origin: "*" the allowed origin is reflected back, so pairing it with credentials: true would let any site read authenticated responses - the exact thing the browser's own ban on Access-Control-Allow-Origin: * plus credentials prevents. Alepha refuses that combination: Access-Control-Allow-Credentials is omitted and a warning is logged at startup. List the origins you trust to enable credentials.
#Rate Limiting
Rate limiting comes from the AlephaServerRateLimit module. Register it and configure the rateLimitOptions atom for a global limit, or attach the $rateLimit middleware to individual actions:
1import { $action } from "alepha/server";2import { $rateLimit } from "alepha/server/rate-limit";3 4class App {5 login = $action({6 use: [$rateLimit({ max: 100, windowMs: 15 * 60 * 1000 })],7 handler: async () => "ok",8 });9}
| Option | Default | Description |
|---|---|---|
max |
100 |
Maximum requests per window |
windowMs |
900000 (15 min) |
Window duration in milliseconds |
keyGenerator |
- | Custom function to generate rate limit keys per request |
skipFailedRequests |
false |
Do not count failed requests |
skipSuccessfulRequests |
false |
Do not count successful requests |
#Per-Action Rate Limiting
Apply rate limits directly on an action. The rateLimit route option is enforced by
AlephaServerRateLimit - it is not part of the base AlephaServer, so without the module
registered the option is silently ignored:
1import { $action } from "alepha/server"; 2import { AlephaServerRateLimit } from "alepha/server/rate-limit"; 3import { Alepha } from "alepha"; 4 5class App { 6 login = $action({ 7 method: "POST", 8 path: "/auth/login", 9 rateLimit: {10 max: 5,11 windowMs: 60 * 1000, // 5 attempts per minute12 },13 handler: async ({ body }) => {14 /* ... */15 },16 });17}18 19Alepha.create().with(AlephaServerRateLimit).with(App);
#Health Check
GET /health and GET /healthz are part of AlephaServer - every server has them, with nothing to import.
1{ "message": "OK", "uptime": 42, "date": "2026-07-31T17:26:36Z", "ready": true }
ready is the field that matters. It follows the container's lifecycle, so it is false for exactly as long as the app is still starting - which is longer than you might expect, because a process binds its port before it runs its migrations.
That gap is why this is not opt-in. A supervisor or load balancer starting your app cannot ask it to expose a readiness endpoint; without one, the best it can do is open a TCP connection, which succeeds while the app is still building its database. It then sends traffic the app cannot serve. Alepha exposes /health universally so the caller can tell listening from working.
Put your reverse proxy in front of it: /health describes your internals and belongs on loopback, not on the public host. Bay returns 404 for it on the public interface.
AlephaServerHealth has been removed - delete the import if you still have one; /health ships with AlephaServer itself.
#Metrics
AlephaServerMetrics exposes a Prometheus-compatible metrics endpoint. Import from alepha/server/metrics.
1import { Alepha } from "alepha";2import { AlephaServerMetrics } from "alepha/server/metrics";3 4Alepha.create().with(AlephaServerMetrics).with(App).start();
Serves metrics in Prometheus text format at /metrics.
Opt-in, unlike /health: it pulls in prom-client, and an app that nothing scrapes should not carry it.
Set METRICS_TOKEN if the app itself is reachable from the network. Alepha warns at startup when it is - production, no token, and SERVER_HOST bound to something other than loopback. An app on loopback behind a proxy gets no warning: the proxy decides what the internet sees.
#Combining Middlewares
Register multiple modules together:
1import { Alepha } from "alepha"; 2import { AlephaServerCors, corsOptions } from "alepha/server/cors"; 3import { 4 AlephaServerRateLimit, 5 rateLimitOptions, 6} from "alepha/server/rate-limit"; 7 8const alepha = Alepha.create() 9 .with(AlephaServerCors)10 .with(AlephaServerRateLimit);11 12alepha.store.mut(corsOptions, (o) => ({13 ...o,14 origin: "https://app.example.com",15}));16alepha.store.mut(rateLimitOptions, (o) => ({17 ...o,18 max: 100,19 windowMs: 15 * 60 * 1000,20}));21 22await alepha.start();
Module order in .with() calls does not affect execution order. Alepha uses hook priorities internally to ensure correct ordering (e.g., CORS headers are set before the handler runs).