VibeWarden + Express / Node.js¶
This guide shows how to put VibeWarden in front of an Express (Node.js) application running in Docker Compose. VibeWarden handles TLS, authentication (Ory Kratos), rate limiting, and security headers. Express receives only authenticated, validated requests with identity injected as HTTP headers.
Architecture¶
Internet
│
▼ :443 (HTTPS)
VibeWarden (Caddy embedded)
│ validates session cookie → Kratos
│ injects X-User-* headers
▼ :3000 (HTTP, internal)
Express app
Your Express app listens on port 3000 on the internal Docker network. It is never directly reachable from the internet — VibeWarden is the sole entry point.
vibewarden.yaml¶
server:
host: "0.0.0.0"
port: 443
upstream:
host: "express" # Docker Compose service name
port: 3000
tls:
enabled: true
provider: letsencrypt
domain: "myapp.example.com"
kratos:
public_url: "http://kratos:4433"
admin_url: "http://kratos:4434"
auth:
public_paths:
- "/login"
- "/register"
- "/recovery"
- "/verification"
- "/public/*"
- "/static/*"
- "/favicon.ico"
- "/robots.txt"
session_cookie_name: "ory_kratos_session"
login_url: "/login"
body_size:
max: "10MB"
overrides:
- path: /api/upload
max: "50MB"
rate_limit:
enabled: true
per_ip:
requests_per_second: 20
burst: 40
per_user:
requests_per_second: 200
burst: 400
trust_proxy_headers: false
log:
level: "info"
format: "json"
admin:
enabled: true
token: "" # set via VIBEWARDEN_ADMIN_TOKEN env var
metrics:
enabled: true
path_patterns:
- "/api/:resource"
- "/api/:resource/:id"
- "/api/v1/:resource"
- "/api/v1/:resource/:id"
security_headers:
enabled: true
hsts_max_age: 31536000
hsts_include_subdomains: true
content_type_nosniff: true
frame_option: "DENY"
content_security_policy: "default-src 'self'"
referrer_policy: "strict-origin-when-cross-origin"
cross_origin_opener_policy: "same-origin"
cross_origin_resource_policy: "same-origin"
suppress_via_header: true
docker-compose.yml¶
services:
postgres:
image: postgres:17-alpine
container_name: myapp-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
kratos-migrate:
image: oryd/kratos:v1.3.1
container_name: myapp-kratos-migrate
restart: on-failure
environment:
DSN: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable
volumes:
- ./config/kratos:/etc/config/kratos:ro
command: migrate sql -e --yes
depends_on:
postgres:
condition: service_healthy
networks:
- myapp
kratos:
image: oryd/kratos:v1.3.1
container_name: myapp-kratos
restart: unless-stopped
environment:
DSN: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable
SERVE_PUBLIC_BASE_URL: https://${DOMAIN}/
SERVE_ADMIN_BASE_URL: http://kratos:4434/
volumes:
- ./config/kratos:/etc/config/kratos:ro
command: serve --config /etc/config/kratos/kratos.yml --watch-courier
depends_on:
kratos-migrate:
condition: service_completed_successfully
networks:
- myapp
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://kratos:4434/admin/health/ready"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
vibewarden:
image: ghcr.io/vibewarden/vibewarden:latest
container_name: myapp-vibewarden
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./vibewarden.yaml:/vibewarden.yaml:ro
- caddy_data:/root/.local/share/caddy
environment:
VIBEWARDEN_KRATOS_PUBLIC_URL: http://kratos:4433
VIBEWARDEN_KRATOS_ADMIN_URL: http://kratos:4434
VIBEWARDEN_UPSTREAM_HOST: express
VIBEWARDEN_UPSTREAM_PORT: "3000"
VIBEWARDEN_SERVER_HOST: "0.0.0.0"
VIBEWARDEN_ADMIN_TOKEN: ${VIBEWARDEN_ADMIN_TOKEN}
depends_on:
kratos:
condition: service_healthy
express:
condition: service_healthy
networks:
- myapp
express:
image: your-registry/your-express-app:latest
container_name: myapp-express
restart: unless-stopped
environment:
NODE_ENV: production
PORT: "3000"
expose:
- "3000"
networks:
- myapp
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
caddy_data:
networks:
myapp:
driver: bridge
Reading X-User-* headers in Express¶
VibeWarden injects identity headers on every authenticated request before forwarding to the upstream. Your Express app reads these headers to identify the current user without making any additional auth calls.
Available headers¶
| Header | Description |
|---|---|
X-User-ID |
Kratos identity UUID |
X-User-Email |
Primary email address from the identity traits |
X-User-Verified |
"true" if the email address has been verified |
X-Session-ID |
Kratos session UUID |
Middleware to extract user identity¶
Create a middleware that reads the VibeWarden-injected headers and attaches the user
to req.user:
// middleware/vibewarden.js
/**
* Reads the X-User-* headers injected by VibeWarden and attaches
* a user object to req.user. Must run after VibeWarden has validated
* the session — never trust these headers if the request did not come
* through VibeWarden.
*/
function vibewardenAuth(req, res, next) {
const userId = req.headers["x-user-id"];
const userEmail = req.headers["x-user-email"];
const verified = req.headers["x-user-verified"] === "true";
const sessionId = req.headers["x-session-id"];
if (!userId) {
// VibeWarden should have redirected unauthenticated requests.
// This is a defensive fallback for routes not covered by VibeWarden.
return res.status(401).json({ error: "unauthorized" });
}
req.user = { id: userId, email: userEmail, verified, sessionId };
next();
}
module.exports = { vibewardenAuth };
TypeScript version:
// middleware/vibewarden.ts
import { Request, Response, NextFunction } from "express";
export interface VibeWardenUser {
id: string;
email: string;
verified: boolean;
sessionId: string;
}
declare global {
namespace Express {
interface Request {
user?: VibeWardenUser;
}
}
}
export function vibewardenAuth(
req: Request,
res: Response,
next: NextFunction
): void {
const id = req.headers["x-user-id"] as string | undefined;
const email = (req.headers["x-user-email"] as string) ?? "";
const verified = req.headers["x-user-verified"] === "true";
const sessionId = (req.headers["x-session-id"] as string) ?? "";
if (!id) {
res.status(401).json({ error: "unauthorized" });
return;
}
req.user = { id, email, verified, sessionId };
next();
}
Applying the middleware¶
Apply globally on all protected routes:
// app.js
const express = require("express");
const { vibewardenAuth } = require("./middleware/vibewarden");
const app = express();
app.use(express.json());
// Health check — public, no auth required.
// Add this path to auth.public_paths in vibewarden.yaml.
app.get("/health", (req, res) => res.json({ status: "ok" }));
// All routes below require authentication via VibeWarden.
app.use(vibewardenAuth);
app.get("/api/me", (req, res) => {
res.json({
id: req.user.id,
email: req.user.email,
});
});
app.get("/api/dashboard", (req, res) => {
res.json({ message: `Hello, ${req.user.email}` });
});
app.listen(3000);
Apply per-router for fine-grained control:
const apiRouter = express.Router();
apiRouter.use(vibewardenAuth);
apiRouter.get("/profile", (req, res) => {
res.json({ userId: req.user.id });
});
app.use("/api", apiRouter);
Health check route¶
Add a /health endpoint that does not require authentication. Register it in
auth.public_paths in vibewarden.yaml so VibeWarden passes it through without a
session check:
Logging¶
VibeWarden emits structured JSON logs. Configure Express to also emit JSON logs so your log aggregation pipeline receives a consistent format:
// Using pino (recommended for production)
const pino = require("pino");
const pinoHttp = require("pino-http");
const logger = pino({ level: "info" });
app.use(
pinoHttp({
logger,
customProps: (req) => ({
userId: req.headers["x-user-id"],
sessionId: req.headers["x-session-id"],
}),
})
);
This enriches every HTTP log entry with the user and session IDs injected by VibeWarden, making it easy to correlate VibeWarden logs with Express logs in your log aggregation platform.