Security & operations
Django's SecurityMiddleware, CSRF protection, ALLOWED_HOSTS, cache and a
reverse proxy come bundled or one pip install away. Leaving the framework
means re-creating that envelope deliberately — and as code, so it ships in
the same .tar as the app. This page is the operations envelope: the cache
stone, the app-level hardening, and the Caddy front door.
Cache — @repo/cache (Valkey)
Valkey (the Redis fork) behind the stack's usual contract: no-ops without
VALKEY_URL, so it's in the compose default profile but never load-bearing.
A small JSON API plus the string store the auth layer uses as its shared
rate-limit backend.
/** Read a JSON value. Misses, disabled cache and errors all return null. */
export async function cacheGet<T>(key: string): Promise<T | null> {
if (!isCacheEnabled()) return null;
try {
const raw = await getClient().get(PREFIX + key);
return raw === null ? null : (JSON.parse(raw) as T);
} catch {
return null;
}
}
/** Write a JSON value with a TTL. A no-op when disabled or unreachable. */
export async function cacheSet(key: string, value: unknown, ttlSeconds: number): Promise<void> {
if (!isCacheEnabled()) return;
try {
await getClient().set(PREFIX + key, JSON.stringify(value), "EX", ttlSeconds);
} catch {
/* degraded — next read is a miss */
}
}
export async function cacheDelete(key: string): Promise<void> {
if (!isCacheEnabled()) return;
try {
await getClient().del(PREFIX + key);
} catch {
/* degraded */
}
}
/** Remaining TTL in seconds, or null when missing/disabled. */
export async function cacheTtl(key: string): Promise<number | null> {
if (!isCacheEnabled()) return null;
try {
const ttl = await getClient().ttl(PREFIX + key);
return ttl >= 0 ? ttl : null;
} catch {
return null;
}
}Failures degrade to a cache miss, never a request failure: a down Valkey makes
the app slower, not broken. Try it at /sandbox/cache — spec in
e2e/sandbox/cache.spec.ts.
Application hardening
These live in example/src/start.ts as global request middleware (every page,
server function and API route) plus the Better Auth config.
| Django concern | Here |
|---|---|
CsrfViewMiddleware | createCsrfMiddleware — header-based (Sec-Fetch-Site/Origin/Referer) on unsafe methods |
ALLOWED_HOSTS | Better Auth trustedOrigins from TRUSTED_ORIGINS (+ BETTER_AUTH_URL) |
LoginAttempt brute-force log | failed sign-ins (401) recorded to the audit log with the client IP |
django-ratelimit | Better Auth rate limit on the shared Valkey store — holds across instances |
watchman health check | /api/health — DB ping + feature flags, 503 when the database is down |
CSRF protection is header-based rather than token-based: TanStack Start is
same-origin, so an unsafe request that lacks a same-origin Sec-Fetch-Site
(or matching Origin/Referer) is refused before any handler runs. No hidden
form token to thread through.
import { createFileRoute } from "@tanstack/react-router";
import { sql } from "drizzle-orm";
import { isCacheEnabled } from "@repo/cache";
import { db } from "@repo/db";
import { isJobsEnabled } from "@repo/jobs";
import { isMetricsEnabled } from "@repo/metrics";
/**
* GET /api/health — the watchman counterpart. 200 with a feature summary
* when the database answers, 503 otherwise. Unauthenticated by design: load
* balancers, Caddy and uptime probes call it. It reveals feature FLAGS, not
* data.
*/
export const Route = createFileRoute("/api/health")({
server: {
handlers: {
GET: async () => {
try {
await db.execute(sql`SELECT 1`);
return Response.json({
status: "ok",
database: true,
jobs: isJobsEnabled(),
cache: isCacheEnabled(),
metrics: isMetricsEnabled(),
});
} catch {
return Response.json({ status: "degraded", database: false }, { status: 503 });
}
},
},
},
});The hardening has its own spec, e2e/hardening.spec.ts: it proves a cross-site
POST is rejected (403), an origin-less POST is rejected, the health endpoint
answers, and a failed sign-in lands in the audit log.
Reverse proxy — Caddy as code
example/caddy/Caddyfile is the front door, opt-in via
docker compose --profile proxy up -d (browse through http://localhost:8088).
The same file is the on-prem starting point — swap the site address for the
real host and tls internal (an internal CA, since Let's Encrypt can't be
reached air-gapped).
It sets the security headers Django's SecurityMiddleware owned — HSTS,
X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Permissions-Policy — strips the Server header, and blocks /api/metrics
from the outside (Prometheus scrapes it from inside the network). The
iprestrict counterpart (an IP allowlist) is a commented example to enable per
deployment.
Air-gapped posture
Everything here is self-hosted and the images are pinned (the latest
drift that broke the Hatchet engine is the cautionary tale). The last CDN
dependency in the app — Google Fonts — is gone: fonts are self-hosted via
@fontsource, so the app renders identically with no internet. What remains
for a sealed network is the intly-CA wiring in Caddy and an internal image
mirror — both deployment configuration, not code changes.