Flatten proxy config to a bool (drop the caddy key)

CaddySpec had only `enable` left (path_prefix/host removed; extra_snippets was
unused), so `proxy: { caddy: {} }` was three levels of nesting for a checkbox —
and it invited drift (supabase silently reverted to proxy.caddy.host). Replace it
with a plain `ServiceSpec.proxy: bool`.

- Model: remove ProxySpec/CaddySpec; `proxy: bool = False`. Readers simplified to
  `bool(svc.proxy)` (generator, castle-api summaries, CLI info).
- CLI create / dashboard editor + create form: write `proxy: true` / a checkbox.
- Configs migrated to `proxy: true` (and supabase's stale proxy.caddy.host block
  removed).
- Docs (registry, dns-and-tls, design, stack guides) + tests/fixtures updated;
  also fixed lingering path-model staleness in the stack guides.
This commit is contained in:
2026-06-30 21:23:14 -07:00
parent f14ef3f40b
commit 770574f575
20 changed files with 47 additions and 77 deletions

View File

@@ -170,9 +170,7 @@ services:
http:
internal: { port: 9001 }
health_path: /health
proxy:
caddy:
path_prefix: /central-context
proxy: true # expose at central-context.<gateway.domain>
manage:
systemd: {}

View File

@@ -12,13 +12,13 @@ browser on your LAN:
2. **Trust** — is the certificate the node serves one the browser accepts? *(TLS)*
Castle answers #1 by leaning on your LAN's own DNS, and #2 with a per-node choice
of three TLS modes. They're orthogonal: you pick a resolution strategy and a trust
of two TLS modes. They're orthogonal: you pick a resolution strategy and a trust
strategy, and any working combination is fine.
## The gateway is the single ingress
Every gateway-reachable service goes through the Caddy **gateway**. Exposure is a
single **checkbox** (`proxy.caddy` on a service):
single **checkbox** (`proxy: true` on a service):
- **unchecked** — the service is reachable only at its own `host:port` (no gateway
route, no DNS name).
@@ -116,7 +116,7 @@ origin — moving a service onto HTTPS changes its origin.
## Putting a service on trusted HTTPS — the recipe
1. **Check the box.** Add `proxy: { caddy: {} }` to the service — it's now exposed
1. **Check the box.** Add `proxy: true` to the service — it's now exposed
at `<service-name>.<gateway.domain>` (rename the service to change the name).
2. **Make the name resolve.** Add (or rely on) the LAN wildcard for the zone
(§DNS). Verify: `dig +short <name>.<domain>` → the node's IP.

View File

@@ -79,8 +79,7 @@ expose:
http:
internal: { port: 9001 }
health_path: /health
proxy:
caddy: {} # expose at my-service.<gateway.domain>
proxy: true # expose at my-service.<gateway.domain>
manage:
systemd: {}
```
@@ -272,13 +271,12 @@ expose:
### `proxy` — Expose the service at a subdomain
`proxy.caddy` is a **checkbox**: present (and `enable: true`, the default) means the
gateway routes **`<service-name>.<gateway.domain>`** to this service; absent means
the service is reachable only at its own `host:port`.
`proxy` is a **checkbox** (a bool): `true` means the gateway routes
**`<service-name>.<gateway.domain>`** to this service; omitted/`false` means the
service is reachable only at its own `host:port`.
```yaml
proxy:
caddy: {} # expose at <service-name>.<gateway.domain>
proxy: true # expose at <service-name>.<gateway.domain>
```
The subdomain is always the service name — there's nothing to customize (rename the
@@ -334,7 +332,7 @@ version number"). `gateway.tls` has two values:
| `acme` | one `*.<domain>` `:443` site | matcher inside the wildcard site | **real Let's Encrypt wildcard, no CA install** |
Path-prefix and static routes always stay on the HTTP `:<port>` site — the way to
put a service on HTTPS is to give it a `proxy.caddy.host`. A node with no public
put a service on HTTPS is to set `proxy: true` (and use acme mode). A node with no public
domain stays on `off` (plain HTTP; use `localhost`/direct ports for anything that
needs a secure context).
@@ -387,7 +385,7 @@ it needs **no inbound exposure and no public A records** for the services. Only
**LAN DNS** resolves `*.<domain>` to the gateway's private IP. (HTTP-01 can't
validate a wildcard, so DNS-01 — and thus the provider token — is mandatory here.)
Every subdomain is the **service name**: a service checks the `proxy.caddy` box and
Every subdomain is the **service name**: a service sets `proxy: true` and
is published at `<name>.<domain>`. Services stay domain-agnostic (switching
`gateway.domain` needs no service edits). One `*.<domain>` site means a single cert
covers every route — adding a service needs no new cert.
@@ -560,8 +558,7 @@ services:
http:
internal: { port: 9001 }
health_path: /health
proxy:
caddy: {} # expose at my-service.<gateway.domain>
proxy: true # expose at my-service.<gateway.domain>
manage:
systemd: {}
```

View File

@@ -123,8 +123,7 @@ services:
http:
internal: { port: 9001 }
health_path: /health
proxy:
caddy: { path_prefix: /my-service }
proxy: true # expose at my-service.<gateway.domain>
manage:
systemd: {}
```

View File

@@ -56,9 +56,9 @@ import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
export default defineConfig({
// Castle sets VITE_BASE to the gateway serve prefix at build time (`/<name>/`,
// or `/` for the root app). Reading it here makes the bundle's absolute asset
// URLs resolve when served behind the gateway at a subpath — no hand-tuning.
// Castle sets VITE_BASE=/ at build time (every frontend serves at its
// subdomain root). Reading it here keeps the bundle.s absolute asset
// URLs resolving at the root — no hand-tuning.
base: process.env.VITE_BASE ?? "/",
plugins: [react(), tailwindcss()],
resolve: {
@@ -157,8 +157,7 @@ services:
expose:
http:
internal: { port: 5173 }
proxy:
caddy: {} # expose the dev server at my-frontend.<gateway.domain>
proxy: true # expose the dev server at my-frontend.<gateway.domain>
```
See @docs/registry.md for the full registry reference.

View File

@@ -117,11 +117,12 @@ the function holds credentials and can meter usage.
## Gateway & secure context
A `public` app is fine on the gateway's HTTP static route at `/my-app/`. An app
that uses **auth or WebCrypto** needs a **secure context**, so give it its own
HTTPS host route instead — `proxy.caddy.host: my-app.lan` with `gateway.tls:
internal` — exactly like the substrate itself (`supabase.lan`). See the "HTTPS for
host routes" section of @docs/registry.md.
A supabase app is a `frontend` program (its `public/` is served in place), so the
gateway serves it at its own subdomain `<name>.<gateway.domain>`. With
`gateway.tls: acme` that subdomain is HTTPS — a **secure context**, which apps
using **auth or WebCrypto** require — with no private CA to install. (The substrate
service itself is likewise at `supabase.<gateway.domain>`.) See
@docs/dns-and-tls.md.
## Commands