Declarative Caddy routes, tunnel configuration, and URL templates for a single service. Read by
asd net applyat apply time.
Every package can declare a net.manifest.yaml that tells asd how the
service should be reached: which upstream to reverse-proxy to, what
hostname routes the traffic, whether a public tunnel is exposed, and
which environment variables to write back to .env.
This page is the reference. For a guided walk-through, start with
learn/01-hello-world. For raw
escape-hatch routing, see learn/06-custom-routing.
| Field | Type | Required | Purpose |
|---|---|---|---|
id |
string (template) | Yes | Unique service identifier |
name |
string | Yes | Display name |
seedDefaultRegistry |
boolean | No | Auto-register in registry.json on apply |
endpoint |
object | Yes | Reverse-proxy upstream target |
caddy |
object | No | Caddy route configuration |
tunnel |
object | No | Tunnel configuration |
env |
object | No | Environment variables to write back to .env |
labels |
object | No | Metadata tags |
endpointThe upstream asd reverse-proxies traffic to.
| Field | Type | Purpose |
|---|---|---|
url |
string (template) | Upstream URL — typically http://127.0.0.1:<PORT> |
endpoint:
url: "http://127.0.0.1:${{ env.MYAPP_PORT }}"
caddyCaddy-specific routing configuration. Pick the simplest field that
covers your case; drop down to rawRoutes only when nothing higher
will do.
| Field | Type | Purpose |
|---|---|---|
publishPreferred |
boolean | Publish this route as the preferred entry for the service |
hostRoute |
object | Declarative hostname-based route (the common case) |
responseHeaders |
object | Add response headers to every response on this route |
authPolicy |
string | Declarative auth policy name (caddy-security forward auth) |
authBypassPaths |
array | Paths that skip the auth policy (e.g. webhooks, health) |
compression |
boolean | Enable gzip + zstd response compression |
forwardedPrefix |
string | Strip a path prefix before forwarding to the upstream |
routes |
array | Extra declarative routes (beyond hostRoute) |
rawRoutes |
array | Raw Caddy JSON routes — full control, no schema validation |
caddy.hostRouteSufficient for almost every service. Generates a standard reverse-proxy
route matching the given host.
caddy:
hostRoute:
host: "${{ macro.concat('env:ENV_PREFIX', 'my-app') }}-${{ macro.tunnelClientId() }}.${{ macro.tunnelEndpoint() }}"
caddy.responseHeaderscaddy:
responseHeaders:
Cache-Control: ["no-store, no-cache, must-revalidate, proxy-revalidate"]
Pragma: ["no-cache"]
Expires: ["0"]
caddy.authPolicy + caddy.authBypassPathsForward auth via caddy-security. Bypass paths skip the auth check
(typical for webhooks, OAuth callbacks, health endpoints).
caddy:
authPolicy: pma-users
authBypassPaths: ["/api/health", "/oauth2callback"]
caddy.rawRoutesEscape hatch when declarative options aren't enough. Uses Caddy's
native JSON route format. Use sparingly — you lose schema validation
and pay in legibility.
caddy:
rawRoutes:
- match:
- host: ["${{ macro.concat('env:ENV_PREFIX', 'auth-app') }}-${{ macro.tunnelClientId() }}.${{ macro.tunnelEndpoint() }}"]
handle:
- handler: reverse_proxy
headers:
request:
set:
X-Forwarded-Proto: ["https"]
X-Forwarded-Host: ["{http.request.host}"]
X-Forwarded-Port: ["443"]
upstreams:
- dial: "127.0.0.1:${{ env.AUTH_PORT }}"
flush_interval: -1
terminal: true
tunnel| Field | Type | Purpose |
|---|---|---|
public |
boolean | Expose the route via a public tunnel (sish) |
tunnel:
public: true
envEnvironment variables asd net apply writes back to .env. Only
written if the variable is not already set — .env is the SSOT for
ephemeral state.
env:
MYAPP_URL: "${{ macro.exposedOrigin() }}"
MYAPP_INTERNAL_URL: "http://127.0.0.1:${{ env.MYAPP_PORT }}"
labelsArbitrary metadata tags. Used by tooling; no runtime effect.
labels:
source: package
priority: "75"
| Macro | Expands to | Example |
|---|---|---|
${{ env.X }} |
Value from .env |
${{ env.MYAPP_PORT }} → 8083 |
${{ macro.tunnelClientId() }} |
ASD_CLIENT_ID from .env |
xyz1 |
${{ macro.tunnelEndpoint() }} |
Tunnel hostname | eu2.tn.example.com |
${{ macro.exposedOrigin() }} |
Full tunnel URL | https://my-app-xyz1.eu2.tn.example.com |
${{ macro.concat('env:X', 'literal') }} |
Concatenation: env var + literal | dev-my-app |
hostRouteThe 95% case: one upstream, one route, one tunnel.
id: "${{ macro.concat('env:ENV_PREFIX', 'my-app') }}"
name: My App
seedDefaultRegistry: true
endpoint:
url: "http://127.0.0.1:${{ env.MYAPP_PORT }}"
caddy:
publishPreferred: true
hostRoute:
host: "${{ macro.concat('env:ENV_PREFIX', 'my-app') }}-${{ macro.tunnelClientId() }}.${{ macro.tunnelEndpoint() }}"
responseHeaders:
Cache-Control: ["no-store, no-cache, must-revalidate, proxy-revalidate"]
tunnel:
public: true
env:
MYAPP_URL: "${{ macro.exposedOrigin() }}"
rawRoutesWhen you need custom headers or non-standard routing.
caddy:
rawRoutes:
- match:
- host: ["${{ macro.concat('env:ENV_PREFIX', 'auth-app') }}-${{ macro.tunnelClientId() }}.${{ macro.tunnelEndpoint() }}"]
handle:
- handler: reverse_proxy
headers:
request:
set:
X-Forwarded-Proto: ["https"]
X-Forwarded-Host: ["{http.request.host}"]
X-Forwarded-Port: ["443"]
upstreams:
- dial: "127.0.0.1:${{ env.AUTH_PORT }}"
flush_interval: -1
terminal: true
caddy:
authPolicy: pma-users
authBypassPaths: ["/api/health", "/oauth2callback"]
| Rule | Reason |
|---|---|
X-Forwarded-Proto MUST be the literal "https" |
Never {http.request.scheme} — the tunnel speaks HTTP internally, so the dynamic value is always http and breaks signed callback URLs. |
X-Forwarded-Proto goes in reverse_proxy.headers.request.set |
Not in a separate headers handler — Caddy's request mutation only fires inside the proxy handler. |
| Host matching uses a wildcard | *-SERVICE-*.${{ macro.tunnelEndpoint() }} if you want one route to cover env-prefix + client-id variants. |
| Bypass routes BEFORE protected routes | Caddy evaluates routes in order; an authPolicy route placed before its bypass will block the bypass. |
| No hardcoded ports | Always ${{ env.SERVICE_PORT }}. The block on hardcoded ports is enforced. |
| No hardcoded hostnames | Always use the template macros above. |
asd? Start at learn/01-hello-world — three lines of YAML and a working tunnel.learn/03-multiple-services.learn/05-add-authentication.asd.yaml (project-level) schema? See reference/asd-yaml.