Skip to content

Routing & HTTP

Mothership sits in front of your fleet as the HTTP layer: external listeners (binds) accept traffic, routes match requests to ships and bays, and built-in middleware handles static files, compression, and CORS preflight caching.

Binds are external listeners declared under [mothership.bind]. Route bind values must match one of these keys:

[mothership.bind]
http = "0.0.0.0:80"
https = "0.0.0.0:443"
ws = "0.0.0.0:8080"
# Bind values support env expansion
# http = "0.0.0.0:$PORT"

ws is just a bind name, not a special protocol type — every bind is a normal TCP or Unix listener.

Behind a load balancer (AWS ELB/ALB, Cloudflare Spectrum, HAProxy, nginx), enable PROXY protocol to preserve client IPs:

[mothership.bind]
http = "0.0.0.0:80" # direct access
https = { addr = "0.0.0.0:443", proxy_protocol = true } # behind LB

This enables HAProxy PROXY protocol v1/v2 with auto-detection — it works with or without the protocol header. On the load balancer side: AWS target groups → “Proxy Protocol v2”, HAProxy → send-proxy-v2, nginx → proxy_protocol on.

Routes map binds to ships/bays with regex patterns, matched in declaration order:

# Object format
routes = [
{ bind = "http", pattern = "/api/.*" },
{ bind = "ws", pattern = "/cable" },
]
# Shorthand format
routes = ["http:/api/.*", "ws:/cable"]
# With path stripping
routes = [
{ bind = "http", pattern = "/api/.*", strip_prefix = "/api" },
]

If a route references a bind name that isn’t defined in [mothership.bind], mothership skips the route and logs a warning.

Route traffic to different backends based on User-Agent class:

# Browsers get the full app
[[fleet.web]]
name = "app"
command = "rails"
routes = [{ bind = "http", pattern = "/.*", ua_filter = "browser" }]
# LLM agents get markdown
[[fleet.web]]
name = "markdown-api"
command = "rails"
routes = [{ bind = "http", pattern = "/.*", ua_filter = "llm" }]
# Bots and crawlers get the static cache
[[fleet.web]]
name = "static-cache"
command = "nginx"
routes = [{ bind = "http", pattern = "/.*", ua_filter = "bot" }]

Available filters:

Filter Matches
browser Chromium, Firefox, Safari browsers
chromium, firefox, safari A specific browser family
llm Claude/Anthropic agents
bot Bots, crawlers, curl, wget, etc.
~pattern Custom regex (e.g. ~MyAgent.*)

Put specific UA filters before catch-all routes — matching is in declaration order.

Mothership computes Ja4H fingerprints for incoming requests. Ja4H fingerprints HTTP header order and values, which detects bots and headless browsers even when they spoof User-Agent. Fingerprints are logged with each request:

DEBUG method=GET path=/ ua_kind=Chromium shields=Some("ge11nn06enus_...")

Shield-based routing (block or redirect suspicious fingerprints) is planned but not implemented yet.

Static directories are served before routing, with fallthrough: longest prefix wins, and if the file isn’t found the request falls through to routes.

[[mothership.static_dirs]]
path = "./public/assets"
prefix = "/assets"
[[mothership.static_dirs]]
path = "./public"
prefix = "/"
bind = "http" # optional: limit to a specific bind
[mothership]
compression = true

Responses are compressed with gzip, deflate, or brotli based on the client’s Accept-Encoding.

Cache backend OPTIONS responses per-origin to keep preflight storms off your app servers:

[mothership]
cors_cache = true # enable with defaults
# Or with custom settings
[mothership.cors_cache]
enabled = true
default_ttl = 3600 # fallback TTL in seconds (default: 3600)
max_entries = 10000 # maximum cache entries (default: 10000)

The cache key includes bind name, host, origin, path, Access-Control-Request-Method, and Access-Control-Request-Headers. TTL is taken from the backend’s Access-Control-Max-Age header when present.

Optional protection against request floods — off by default:

[mothership.rate_limiting]
global_rps = 10000.0 # global requests per second
per_ip_rpm = 100.0 # per-IP requests per minute

Limited clients receive 429 Too Many Requests with a Retry-After header.