Transport security
Four createApp options cover TLS, proxying, CORS, and response headers: tls,
trustProxy, cors, and security. See the createApp reference
for the full option list.
TLS (native https/wss)
Section titled “TLS (native https/wss)”import { readFileSync } from 'fs';
const app = createApp({ modules: [App], tls: { key: readFileSync('key.pem'), cert: readFileSync('cert.pem') },});When tls is set the server serves https, and WebSocket upgrades become wss on the same
port — no other changes.
trustProxy
Section titled “trustProxy”const app = createApp({ modules: [App], trustProxy: true });Behind a reverse proxy, reads X-Forwarded-Proto / X-Forwarded-For to populate
ctx.protocol ('http' | 'https') and ctx.ip (read via @ctx()). Off by default —
forwarded headers are ignored (and spoofable) until you opt in. It trusts the immediate hop
unconditionally; there’s no CIDR allowlist yet.
Off unless origins is set.
const app = createApp({ modules: [App], cors: { origins: 'https://app.example.com', credentials: true },});origins accepts string | string[] | '*' | (origin) => boolean. A preflight OPTIONS
request (with Access-Control-Request-Method) is auto-answered with 204 before explicit or
automatic OPTIONS routing. A bare OPTIONS request is instead handled by an explicit @Options
route or the router’s automatic 204 + Allow response.
- With
credentials: true, the server never sendsAccess-Control-Allow-Origin: *— it echoes the concrete allowed origin (or blocks the request).
When origins is a predicate
Section titled “When origins is a predicate”A predicate is the reason the option takes a function at all: the shapes it exists for are lookups — an allowlist in Redis, a tenant query, a regex over a parsed URL. Two things are worth knowing before you put I/O in one.
It is consulted once per request. Once for a normal request, once for a preflight, and not at all
when the request carries no Origin header — the predicate is only reached when there is an origin
to judge. You can put a round-trip in it without it being charged twice.
A predicate that throws denies the origin. The failure is logged and the request is answered without CORS headers, so the browser blocks it. It is not a 500 and it is not an open door: a lookup that failed has not said yes, and a backing store being briefly unavailable must never widen an allowlist. Watch the logs for it — from the outside, a broken lookup and a genuinely disallowed origin look the same to the caller, which is the price of failing closed.
security (headers, ON by default)
Section titled “security (headers, ON by default)”Applied to every HTTP response. Disable everything with security: false, or override
individual headers with an options object.
| Header | Default |
|---|---|
X-Content-Type-Options |
nosniff |
X-Frame-Options |
SAMEORIGIN |
Referrer-Policy |
no-referrer |
X-DNS-Prefetch-Control |
off |
Strict-Transport-Security |
max-age=15552000 (180d) — only when the connection is secure (native TLS, or trustProxy + X-Forwarded-Proto: https); no includeSubDomains/preload |
Content-Security-Policy |
not set — pass security: { csp: "..." } |
const app = createApp({ modules: [App], security: { csp: "default-src 'self'" } });Defaults
Section titled “Defaults”| Option | Default |
|---|---|
tls |
off (plain http/ws) |
trustProxy |
off |
cors |
off (same-origin only) |
security |
ON (HSTS only when the connection is secure) |
Related
Section titled “Related”createAppreference — every construction option- File uploads / multipart — request-body handling