# lith production Caddyfile — full subdomain routing
# Cloudflare handles TLS. Caddy serves HTTP on :80.
# --- papers.aesthetic.computer ---
# (touched 2026-04-29 to re-fire webhook after the install-Caddyfile-before-reload fix)
:443 {
tls /etc/caddy/origin-cert.pem /etc/caddy/origin-key.pem
log {
output file /var/log/caddy/access.log {
roll_size 50MiB
roll_keep 3
roll_keep_for 72h
}
format json
level INFO
}
# --- Global performance headers ---
# Code modules change every deploy — short TTL so rollouts are visible fast.
# (lib/disk .mjs are STATIC sub-imports without the ?v= cache-bust boot.mjs
# applies to top-level modules, so a long TTL pins clients to stale runtime
# code. The deploy script also purges the CDN; this bounds browser-side
# staleness to ~1min.)
@code path *.mjs *.js *.css *.lisp *.lua
header @code Cache-Control "public, max-age=60, stale-while-revalidate=300"
# `?` = set only if absent. The upstream Express app (server.mjs) already
# sends Access-Control-Allow-Origin on proxied responses; adding a second
# copy here produced "*, *", which browsers reject (the same bug the IPFS
# block below hit). Set-if-absent yields exactly one header for both proxied
# and Caddy-served responses.
header @code ?Access-Control-Allow-Origin *
# Static assets: long cache (1h fresh, serve stale for 24h while revalidating)
@cacheable path *.woff2 *.woff *.ttf *.png *.jpg *.jpeg *.svg *.gif *.webp *.ico *.mp3 *.wav *.mp4 *.json
header @cacheable Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
header @cacheable ?Access-Control-Allow-Origin * # set-if-absent — see @code note
# Service workers must always revalidate — cached sw.js delays rollout of
# bumped CACHE_NAME, pinning clients to stale module caches.
@serviceworker path /sw.js /firebase-messaging-sw.js
header @serviceworker Cache-Control "no-cache, no-store, must-revalidate"
# HTML: no-cache (always revalidate, but use ETag for 304)
@html path / *.html
header @html Cache-Control "no-cache"
# Encode everything (Caddy does this by default, but be explicit)
encode zstd gzip
@papers host papers.aesthetic.computer papers.prompt.ac
handle @papers {
handle /en {
rewrite * /index.html?lang=en
root * /opt/ac/system/public/papers.aesthetic.computer
file_server
}
handle /da {
rewrite * /index.html?lang=da
root * /opt/ac/system/public/papers.aesthetic.computer
file_server
}
handle /es {
rewrite * /index.html?lang=es
root * /opt/ac/system/public/papers.aesthetic.computer
file_server
}
handle /cn {
rewrite * /index.html?lang=zh
root * /opt/ac/system/public/papers.aesthetic.computer
file_server
}
root * /opt/ac/system/public/papers.aesthetic.computer
# /platter is the existing white research-records page (platter.html).
# We briefly created /platter/ as a directory with our own index — that
# was the wrong call; restored below by deleting the dir's index and
# reversing the prior 301 with a 302 to invalidate cached redirects.
# /platter/jeffrey/ remains a real directory (the dashboard); its
# explicit canonical redirect stays so the page's relative
# `./manifest.json` fetch resolves correctly.
# (See: 2026-04-29 platter restoration.)
redir /platter/ /platter 302
redir /platter/jeffrey /platter/jeffrey/ permanent
# SPA fallback for unknown paths: if none of {path}, {path}.html,
# {path}/index.html, or {path}index.html exists on disk, serve
# /index.html — but mark it no-cache. Without this, a request for a
# not-yet-deployed PDF gets the SPA HTML, and Cloudflare pins that
# HTML to the PDF URL for 4h via its default static-asset cache.
# (See: 2026-04-26 latency-paper deploy gap.) The two index.html
# terms cover both /platter (no slash → {path}/index.html) and
# /platter/ (slash → {path}index.html). (See: 2026-04-29
# jeffrey-platter URL move.)
@missing not file {
try_files {path} {path}.html {path}/index.html {path}index.html
}
handle @missing {
header Cache-Control "no-cache, must-revalidate, max-age=0"
rewrite * /index.html
file_server
}
# NOTE: {path} is intentionally absent from the top-level try_files.
# Caddy's try_files matches directories as well as files, so including
# {path} would silently match a request for /platter (the directory),
# short-circuit before {path}/index.html, then file_server would serve
# the wrong content rather than canonicalizing to /platter/. Without
# {path} here, file_server handles existing regular files itself and
# issues a 301 redirect for directory-without-slash to the canonical
# /
/ form. (See: 2026-04-29 platter URL move.)
try_files {path}.html {path}/index.html {path}index.html
file_server
}
# --- bills.aesthetic.computer ---
@bills host bills.aesthetic.computer
handle @bills {
root * /opt/ac/system/public/bills.aesthetic.computer
try_files {path} {path}.html /index.html
file_server
}
# --- nela.aesthetic.computer (NELA Computer Club donate page) ---
@nela host nela.aesthetic.computer
handle @nela {
root * /opt/ac/system/public/nela.aesthetic.computer
try_files {path} {path}.html /index.html
file_server
}
# --- give.aesthetic.computer ---
@give host give.aesthetic.computer
handle @give {
handle /api/* {
reverse_proxy localhost:8888
}
handle /da {
rewrite * /index.html?lang=da¤cy=dkk
root * /opt/ac/system/public/give.aesthetic.computer
file_server
}
handle /es {
rewrite * /index.html?lang=es¤cy=usd
root * /opt/ac/system/public/give.aesthetic.computer
file_server
}
handle /de {
rewrite * /index.html?lang=de¤cy=eur
root * /opt/ac/system/public/give.aesthetic.computer
file_server
}
handle /cn {
rewrite * /index.html?lang=zh¤cy=usd
root * /opt/ac/system/public/give.aesthetic.computer
file_server
}
# Static fallback MUST be wrapped in `handle {}` so it's mutually
# exclusive with `handle /api/*`. Bare `try_files`/`file_server` is
# reordered ahead of `handle` by Caddy's default directive order, which
# rewrites /api/* POSTs to /index.html → file_server 405s them (this
# broke the give-portal cancel button). See news/feed blocks below.
handle {
root * /opt/ac/system/public/give.aesthetic.computer
try_files {path} {path}.html /index.html
file_server
}
}
# --- data.aesthetic.computer (Linked Open Data / CIDOC CRM) ---
# Most paths go to lith, which rewrites them to the `crm` function (landing,
# /@handle, /painting, /piece, /mood, /.well-known/void). /sparql is the one
# exception: it goes straight to the Oxigraph SPARQL store (Stage 2).
@data host data.aesthetic.computer
handle @data {
# Read-only SPARQL: rewrite to Oxigraph's /query endpoint. Its /update
# and /store write endpoints are NEVER proxied — writes happen only
# locally via the ETL (crm/build-graph.mjs).
handle /sparql* {
rewrite * /query
reverse_proxy localhost:7878
}
handle {
reverse_proxy localhost:8888
}
}
# --- news.aesthetic.computer ---
@news host news.aesthetic.computer
handle @news {
handle /api/* {
reverse_proxy localhost:8888
}
handle {
rewrite * /api/news{uri}
reverse_proxy localhost:8888
}
}
# --- api.aesthetic.computer ---
@apidomain host api.aesthetic.computer api.prompt.ac
handle @apidomain {
reverse_proxy localhost:8888
}
# --- feed.aesthetic.computer (DP-1 Feed V2 — Go + Postgres) ---
@feed host feed.aesthetic.computer
handle @feed {
header Access-Control-Allow-Origin *
handle /api/* {
reverse_proxy localhost:8787
}
handle /health {
reverse_proxy localhost:8787
}
handle {
root * /opt/dp1-feed
rewrite * /landing-page.html
file_server
}
}
# --- ipfs.aesthetic.computer (self-hosted IPFS gateway) ---
# Kubo emits Access-Control-Allow-Origin: * on its own. An earlier Caddy
# `header` directive was stacking a second copy, producing "*, *" that
# browsers reject. Let Kubo own the header, Caddy just proxies.
@ipfs host ipfs.aesthetic.computer
handle @ipfs {
reverse_proxy localhost:8090
}
# --- justanothersystem.org ---
@wwwjas2 host www.justanothersystem.org
handle @wwwjas2 {
redir https://justanothersystem.org{uri} 301
}
@jas host justanothersystem.org
handle @jas {
handle /api/* {
reverse_proxy localhost:8888
}
redir /bio /cv 301
redir /bio/ /cv 301
# Static fallback wrapped in handle{} so handle /api/* stays terminal —
# bare try_files is reordered ahead of handle and 405s /api/* POSTs.
handle {
root * /opt/ac/system/public/justanothersystem.org
try_files {path} {path}.html /index.html
file_server
}
}
# --- builds.false.work ---
@builds_api {
host builds.false.work
path /api/* /.netlify/functions/*
}
handle @builds_api {
reverse_proxy localhost:8888
}
@builds host builds.false.work
handle @builds {
root * /opt/ac/system/public/builds.false.work
try_files {path} {path}.html /index.html
file_server
}
# --- sotce.net ---
@sotce host sotce.net www.sotce.net
handle @sotce {
handle /api/* {
reverse_proxy localhost:8888
}
handle /user {
reverse_proxy localhost:8888
}
handle /handle {
reverse_proxy localhost:8888
}
handle /authorized {
reverse_proxy localhost:8888
}
handle /aesthetic.computer/* {
uri strip_prefix /aesthetic.computer
root * /opt/ac/system/public/aesthetic.computer
file_server
}
# Everything else → sotce-net function
handle {
rewrite * /api/sotce-net{uri}
reverse_proxy localhost:8888
}
}
# --- kidlisp.com subdomains ---
@keep host keep.kidlisp.com
handle @keep {
handle /api/* {
reverse_proxy localhost:8888
}
handle /technology {
root * /opt/ac/system/public/kidlisp.com
rewrite * /keeps-tech.html
file_server
}
handle /wallet {
root * /opt/ac/system/public/kidlisp.com
rewrite * /wallet/index.html
file_server
}
handle {
root * /opt/ac/system/public/kidlisp.com
rewrite * /keeps.html
file_server
}
}
@buy host buy.kidlisp.com
handle @buy {
root * /opt/ac/system/public/kidlisp.com
rewrite * /buy.html
file_server
}
@pj host pj.kidlisp.com
handle @pj {
root * /opt/ac/system/public/kidlisp.com
rewrite * /pj.html
file_server
}
@device host device.kidlisp.com
handle @device {
handle /api/* {
reverse_proxy localhost:8888
}
handle /js/* {
root * /opt/ac/system/public/kidlisp.com
file_server
}
handle /qr/* {
root * /opt/ac/system/public/kidlisp.com
rewrite * /qr.html
file_server
}
handle {
root * /opt/ac/system/public/kidlisp.com
rewrite * /device.html
file_server
}
}
@topcalm host top.kidlisp.com calm.kidlisp.com
handle @topcalm {
handle /js/* {
root * /opt/ac/system/public/kidlisp.com
file_server
}
reverse_proxy localhost:8888
}
@learn host learn.kidlisp.com
handle @learn {
handle /api/* {
reverse_proxy localhost:8888
}
handle /decree* {
root * /opt/ac/system/public/kidlisp.com
rewrite * /decree.html
file_server
}
handle {
root * /opt/ac/system/public/kidlisp.com
rewrite * /learn.html
file_server
}
}
@keepsredirect host keeps.kidlisp.com
handle @keepsredirect {
redir https://keep.kidlisp.com{uri} 301
}
@keepsac host keeps.aesthetic.computer
handle @keepsac {
reverse_proxy localhost:8888
}
# --- jas.life ---
@jaslife host jas.life
handle @jaslife {
root * /opt/ac/system/public/jas.life
try_files {path} {path}.html /index.html
file_server
}
# --- www.jas.life redirect ---
@wwwjas host www.jas.life
handle @wwwjas {
redir https://jas.life{uri} 301
}
# --- pals.aesthetic.computer ---
# pals.aesthetic.computer → /api/logo (dynamic logo generation)
@pals host pals.aesthetic.computer
handle @pals {
rewrite * /api/logo{uri}
reverse_proxy localhost:8888
}
@l5prompt host l5.prompt.ac
handle @l5prompt {
redir https://l5.aesthetic.computer{uri} 301
}
@p5prompt host p5.prompt.ac
handle @p5prompt {
redir https://p5.aesthetic.computer{uri} 301
}
@procprompt host processing.prompt.ac
handle @procprompt {
redir https://processing.aesthetic.computer{uri} 301
}
@siteprompt host sitemap.prompt.ac
handle @siteprompt {
redir https://sitemap.aesthetic.computer{uri} 301
}
@apiprompt host api.prompt.ac
handle @apiprompt {
redir https://api.aesthetic.computer{uri} 301
}
# --- legacy duckweedtri hosts ---
@duckweedaesthetic host duckweedtri.aesthetic.computer
handle @duckweedaesthetic {
redir https://aesthetic.computer{uri} 301
}
@duckweedprompt host duckweedtri.prompt.ac
handle @duckweedprompt {
redir https://aesthetic.computer{uri} 301
}
# --- www.prompt.ac redirect ---
# (needs a proxied `www` A record in the prompt.ac zone — without it the
# `*.prompt.ac → 100::` Worker wildcard swallows www and 522s.)
@wwwpromptac host www.prompt.ac
handle @wwwpromptac {
redir https://prompt.ac{uri} 301
}
# --- prompt.ac/menuband → menuband.app (Menu Band's own domain) ---
# Menu Band now lives at menuband.app; this legacy path keeps working by
# forwarding into it (subpath preserved: /menuband/privacy.html →
# menuband.app/privacy.html). More specific than the host-only @promptac
# below, so Caddy evaluates it first.
@promptac_menuband {
host prompt.ac
path /menuband /menuband/*
}
handle @promptac_menuband {
# route{} preserves written order so strip_prefix runs before redir
# captures {uri} — otherwise the target keeps the /menuband prefix.
route {
uri strip_prefix /menuband
redir https://menuband.app{uri} 301
}
}
# --- prompt.ac (apex root only) → HTML prompt shell (its own product) ---
# Naked prompt.ac serves the fast HTML prompt that hosts the AC runtime in
# an iframe. Every other prompt.ac/ still 301s to aesthetic.computer
# (below), so deep links keep working. Evaluated before the redirect since
# it appears first in source order.
@promptac_root {
host prompt.ac
path /
}
handle @promptac_root {
root * /opt/ac/system/public/prompt.ac
rewrite * /index.html
file_server
}
# --- prompt.ac → aesthetic.computer (everything else, 301) ---
# `not path /` so the apex root falls through to @promptac_root above.
# Caddy compiles this host-only handle ahead of the path-/ root handle
# (route order is not strict source order), so without the exclusion this
# catch-all would swallow `/` before the shell handler is reached.
@promptac {
host prompt.ac
not path /
}
handle @promptac {
redir https://aesthetic.computer{uri} 301
}
@tryshared {
host l5.aesthetic.computer processing.aesthetic.computer
path /aesthetic.computer/*
}
handle @tryshared {
root * /opt/ac/system/public
file_server
}
# --- l5.aesthetic.computer ---
@l5 host l5.aesthetic.computer
handle @l5 {
handle /api/* {
reverse_proxy localhost:8888
}
handle /docs* {
reverse_proxy localhost:8888
}
# Static fallback wrapped in handle{} so handle /api/* stays terminal —
# bare try_files is reordered ahead of handle and 405s /api/* POSTs.
handle {
root * /opt/ac/system/public/l5.aesthetic.computer
try_files {path} {path}.html /index.html
file_server
}
}
# --- processing.aesthetic.computer ---
@processing host processing.aesthetic.computer
handle @processing {
handle /api/* {
reverse_proxy localhost:8888
}
handle /docs* {
reverse_proxy localhost:8888
}
# Static fallback wrapped in handle{} so handle /api/* stays terminal —
# bare try_files is reordered ahead of handle and 405s /api/* POSTs.
handle {
root * /opt/ac/system/public/processing.aesthetic.computer
try_files {path} {path}.html /index.html
file_server
}
}
# --- rdp.jas.life ---
@rdp host rdp.jas.life
handle @rdp {
root * /opt/ac/system/public/rdp.jas.life
try_files {path} {path}.html /index.html
file_server
}
# --- quiltnet.org ---
@quiltnet host quiltnet.org www.quiltnet.org
handle @quiltnet {
root * /opt/ac/system/public/quiltnet.org
try_files {path} {path}.html /index.html
file_server
}
# --- kidlisp.com root domain ---
@kidlisproot host kidlisp.com www.kidlisp.com
handle @kidlisproot {
handle /api/* {
reverse_proxy localhost:8888
}
handle /.netlify/functions/* {
reverse_proxy localhost:8888
}
handle /keeps {
redir https://keep.kidlisp.com/ 301
}
# Serve AC runtime assets for iframe embedding
handle /aesthetic.computer/* {
root * /opt/ac/system/public
file_server
}
# kidlisp.com SPA
handle {
root * /opt/ac/system/public/kidlisp.com
try_files {path} {path}.html /index.html
file_server
}
}
# --- notepat.com ---
@notepatroot host notepat.com www.notepat.com
handle @notepatroot {
@notepatindex path /
handle @notepatindex {
rewrite * /notepat
}
# Permalink: notepat.com/amxd → current notepat.com.amxd as a
# direct download. Clients get the Content-Disposition so it
# lands in ~/Downloads instead of opening in-browser.
handle /amxd {
rewrite * /m4l/notepat.com.amxd
header Content-Disposition "attachment; filename=\"notepat.com.amxd\""
root * /opt/ac/system/public
file_server
}
reverse_proxy localhost:8888
}
# --- wipppps.world ---
@wippppsroot host wipppps.world www.wipppps.world
handle @wippppsroot {
@wippppsindex path /
handle @wippppsindex {
rewrite * /wipppps
}
reverse_proxy localhost:8888
}
@mainspa host aesthetic.computer www.aesthetic.computer lith.aesthetic.computer notepat.com www.notepat.com wipppps.world www.wipppps.world p5.aesthetic.computer sitemap.aesthetic.computer
handle @mainspa {
# Assets → DO Spaces CDN. The bucket root maps directly to
# assets.aesthetic.computer (no /assets/ prefix exists in the
# bucket), so we use handle_path to strip the matched /assets/
# prefix before substituting {path} into the redirect target.
# The previous form (`handle /assets/*` + `{uri}`) doubled the
# prefix and produced 403s on assets.aesthetic.computer/assets/...
# (See: 2026-04-29 platter readings 403 fix.)
handle_path /assets/* {
redir https://assets.aesthetic.computer{path} 302
}
# API → lith
handle /api/* {
reverse_proxy localhost:8888
}
handle /.netlify/functions/* {
reverse_proxy localhost:8888
}
# Media → lith
handle /media/* {
reverse_proxy localhost:8888
}
# Static rewrite shortcuts
handle /disks/* {
uri strip_prefix /disks
root * /opt/ac/system/public/aesthetic.computer/disks
file_server
}
handle /lib/* {
uri strip_prefix /lib
root * /opt/ac/system/public/aesthetic.computer/lib
file_server
}
# Static files, then SPA fallback
handle {
root * /opt/ac/system/public
@static file
handle @static {
file_server
}
handle {
reverse_proxy localhost:8888
}
}
}
# --- false.work ---
@falseroot host false.work
handle @falseroot {
root * /opt/ac/system/public/false.work
try_files {path} {path}.html /index.html
file_server
}
@wwwfalse host www.false.work
handle @wwwfalse {
redir https://false.work{uri} 301
}
# --- menuband.app (Menu Band — Mac App Store landing + privacy page) ---
# The landing page uses absolute asset paths (/menuband/icon.png,
# /aesthetic.computer/cursors/…), so those two prefixes are served from the
# full public tree; everything else (apex → index.html, /privacy → privacy)
# is served from the scoped menuband bundle. Each branch is wrapped in its
# own handle{} — mixing bare try_files/file_server with nested handles gets
# reordered by Caddy and breaks the reload (see the l5/kidlisp blocks).
@menubandapp host menuband.app
handle @menubandapp {
handle /menuband/* {
root * /opt/ac/system/public
file_server
}
handle /aesthetic.computer/* {
root * /opt/ac/system/public
file_server
}
handle {
root * /opt/ac/system/public/menuband
try_files {path} {path}.html /index.html
file_server
}
}
@wwwmenubandapp host www.menuband.app
handle @wwwmenubandapp {
redir https://menuband.app{uri} 301
}
# --- Fallback for any unmatched host ---
handle {
reverse_proxy localhost:8888
}
}
:80 {
redir https://{host}{uri} 301
}