Skip to content
Talk to our solutions team

Usage

The kis.ai API gateway (binary gateway.svc) is a TLS-terminating, multi-tenant edge proxy and static/SPA file server. It embeds Traefik v3 in process and translates a kis.ai-flavoured YAML config into Traefik’s dynamic configuration, adding CPET (tenant) resolution, header/path transforms, and metric/telemetry middleware on top.

This guide is the quickstart and the how-to. For the concepts behind it, see the design docs. Every example named below is embedded in the binary — gateway.svc config generate <name> prints it.

Local dev, HTTP-only, no certs, runs without root:

Terminal window
# 1. Bootstrap file (listeners, log level, where routing lives)
gateway.svc config generate quickstart > .gateway.yaml
# 2. A routing config, dropped into the config/ directory
gateway.svc config generate spa-http -d config/
# 3. Content for the SPA example to serve
mkdir -p public && echo '<h1>hello gateway</h1>' > public/index.html
# 4. Run it
gateway.svc -f .gateway.yaml -L debug
# 5. Hit it
curl http://localhost:8443/

See what else ships:

Terminal window
gateway.svc config list # every embedded example + one-liner
gateway.svc config generate single-app # print one to stdout
gateway.svc config generate common -d config/ # write config/common.yaml
gateway.svc config generate all -d config/ # write every example

For production, start from bootstrap instead of quickstart (port 443, zerotrust: true).

The gateway reads two kinds of configuration, and they are kept separate:

KindWhat it isWhere it comes from
Boot / service configListeners, log level, TLS posture, datacenter/cluster, and the pointer to the routing configThe -f bootstrap file, plus the chassis config: block (local folder or remote config.svc)
Routing configdomainmaps, backends, endpoints, routes, staticpaths — the gateway’s own data modelInline in the bootstrap, or a local directory (config.router.dir), or served by config.svc (config.url)

The config: block selects how the service config is sourced (this is the fleet-wide chassis convention — same keys as every other kis.ai service):

config:
url: http://config:5011 # REMOTE: fetch service+cluster config from config.svc
apikey: <api-key> # auth for the remote config service (optional)
cache: /var/lib/gateway/cfg.db # bbolt warm-start cache (optional)
# path: ./service-config # LOCAL service-config folder (alternative to url)
router:
dir: ./config # LOCAL gateway ROUTING config directory (see below)
  • config.url → remote mode. config.svc serves the service config and the routing model (the same top-level domainmaps/routes/backends/… keys), streamed over SSE and hot-reloaded.
  • config.router.dir → local mode. A directory of routing YAML files in the gateway’s multi-file format, loaded and hot-reloaded by the gateway itself.
  • Neither, with routing inline in the bootstrap → “bootstrap-as-config”; the -f file is the whole config. (This is what most of the embedded single-file examples do.)

Note — config.router.dir vs config.path. config.path is the chassis’s service-config folder (lib-config flat-path format). The gateway’s routing directory is a different format and uses its own key, config.router.dir. Don’t put routing files under config.path — the chassis will try to parse them as service config and fail.

There is no --config_dir flag — the routing directory is the config.router.dir key in the bootstrap file.

name: API Gateway # service name (optional)
log: stdout # stdout | host:port (fluentd)
loglevel: info # info | debug | warn | error
host: "" # bind address; "" = all interfaces
port: 8443 # public entrypoint (443 in prod)
zerotrust: false # true = TLS termination + mTLS on the internal entrypoint
internal:
port: 8444 # localhost-only entrypoint (required)
trustedips: [127.0.0.1] # trusted proxies (applied when zerotrust: true)
timeouts: # gateway-wide listener timeouts (per-entrypoint overridable)
read: 10s
write: 10s
idle: 180s

A request flows through five named concepts:

Host header ──► domainmap ──► (Customer, Product, Environment, Tenant)
route ── binds a domainmap to an entrypoint and a list of ──┐
│ │
endpoints ──► backend (a pool of URLs) │
│ │
staticpaths ──► a folder served by a spawned fileserver
entrypoint (a listener: websecure / websecureinternal / custom)
  • domainmaps map one or more Host values to a CPET tuple. The customer-domain-mapper middleware resolves the incoming host and injects X-Customer, X-Product, X-Env, X-Tenant headers so backends know which tenant a request belongs to.
  • backends are named pools of upstream URLs.
  • endpoints match a path prefix and forward to a backend, applying an ordered middleware chain.
  • routes tie a domainmap + an entrypoint to a set of endpoints and/or staticpaths.
  • entrypoints are the listeners. With no explicit entrypoints: block the gateway synthesises two from the boot keys: websecure (:{port}) and websecureinternal (:{internal.port}).

How the routing domain is resolved (running behind a proxy)

Section titled “How the routing domain is resolved (running behind a proxy)”

The routing domain comes from the request’s own Host/authority — a port-less Host on the standard ports (:443/:80) resolves correctly. An incoming X-Forwarded-Host is trusted only from a peer listed in that entrypoint’s trustedips; from any other source it is ignored (and all X-Forwarded-* are stripped), so a client can’t choose the routing domain — or force an SNI/route mismatch — by spoofing the header.

A direct-to-internet gateway needs nothing extra: clients connect with the real Host, and that’s what routing uses. But if you run the gateway behind a reverse proxy or load balancer that terminates the client connection and forwards the real host in X-Forwarded-Host (e.g. kong, which sends Host: localhost), you must list that proxy’s IP in the entrypoint’s trustedips — otherwise the gateway routes by the proxy’s Host (e.g. localhost) and nothing matches:

entrypoints:
- name: websecure
address: ":443"
protocol: http
trustedips: [10.0.1.100] # the upstream proxy/LB; its X-Forwarded-Host is honored
# gateway.svc config generate single-app
backends:
- backend:
name: svc
urls:
- http://127.0.0.1:3000
endpoints:
- name: to-svc
path: { prefix: / }
backend: svc
middlewares:
- customer-domain-mapper: { default: true }
routes:
- name: main
domainmap: main
entrypoint: websecure
endpoints: [to-svc]
domainmaps:
- name: main
domains: [myapp.kisai.dev]
customer: kisai
product: myapp
environment: prod
tenant: main

Every request to myapp.kisai.dev is forwarded to http://127.0.0.1:3000, and the backend receives X-Customer: kisai, X-Product: myapp, X-Env: prod, X-Tenant: main.

Several services behind one domain (path prefixes)

Section titled “Several services behind one domain (path prefixes)”

Route different path prefixes to different backends, stripping the prefix before forwarding:

backends:
- backend: { name: config, urls: [https://127.0.0.1:7123] }
- backend: { name: janus, urls: [https://127.0.0.1:7101] }
- backend: { name: seshat, urls: [https://127.0.0.1:7102] }
endpoints:
- name: to-config
path: { prefix: /config/ }
backend: config
middlewares:
- replaceRegexPrefix: { regex: "^/config/(.*)", replacement: "/${1}" }
- customer-domain-mapper: { default: true }
- name: to-janus
path: { prefix: /account/ }
backend: janus
middlewares:
- replaceRegexPrefix: { regex: "^/account/(.*)", replacement: "/${1}" }
- customer-domain-mapper: { default: true }
- name: to-seshat
path: { prefix: /data/ }
backend: seshat
middlewares:
- replaceRegexPrefix: { regex: "^/data/(.*)", replacement: "/${1}" }
- customer-domain-mapper: { default: true }
routes:
- name: main
domainmap: main
entrypoint: websecure
endpoints: [to-config, to-janus, to-seshat]

replaceRegexPrefix rewrites /config/foo/foo before the request reaches the config backend. (The slice-2 rewrite: block, below, is the newer way to express the same thing.)

Give each tenant its own subdomain and domainmap; share the backends and endpoints:

# gateway.svc config generate multi-tenant
routes:
- name: tenant-a
domainmap: tenant-a
entrypoint: websecure
endpoints: [to-config, to-janus, to-seshat]
- name: tenant-b
domainmap: tenant-b
entrypoint: websecure
endpoints: [to-config, to-janus, to-seshat]
domainmaps:
- name: tenant-a
domains: [tenant-a.forge.kisai.dev]
customer: kisai
product: forge
environment: prod
tenant: tenant-a
- name: tenant-b
domains: [tenant-b.forge.kisai.dev]
customer: kisai
product: forge
environment: prod
tenant: tenant-b

The same backend pool serves both tenants; the customer-domain-mapper stamps the right X-Tenant per request so the backend can isolate data.

backends:
- backend:
name: api
urls:
- http://10.0.0.1:8080
- http://10.0.0.2:8080 # >1 URL ⇒ round-robin by default
https: true # upgrade to https when zerotrust:true (default true)
profile: sticky-session # opt into a named load-balance profile (see below)

The gateway can serve a folder directly: it spawns its own small file server on the URL you give and proxies the route to it. Two types:

typeBehaviour
staticServes files as-is. Missing file → notfoundpath (with 404) if set, else plain 404.
spaSame, but a missing path falls back to notfoundpath (your index.html) so the SPA’s client-side router can take over. This is what makes deep links work.
# gateway.svc config generate spa
staticpaths:
- name: build
type: spa
folderpath: /var/www/app # your built output (Vite ./dist, CRA ./build, …)
notfoundpath: index.html # SPA fallback — serve index.html for unknown paths
indexpath: index.html
path: { prefix: / }
services:
- url: http://localhost:7151 # gateway spawns its fileserver on this local port
middlewares:
- customer-domain-mapper: { default: true }
routes:
- name: main
domainmap: main
entrypoint: websecure
staticpaths: [build] # note: staticpaths, not endpoints

Build your SPA with the correct base path (base: '/' for Vite, "homepage": "." for CRA), or deep links and asset URLs will break under a sub-path.

spa-http pairs with the quickstart bootstrap — HTTP-only, catch-all on /, matches localhost/127.0.0.1, no common.yaml needed:

# gateway.svc config generate spa-http
staticpaths:
- name: site
type: spa
folderpath: ./public
notfoundpath: index.html
indexpath: index.html
path: { prefix: / }
services:
- url: http://localhost:7150
middlewares:
- customer-domain-mapper: { default: true }
routes:
- name: local
domainmap: local
entrypoint: websecure # plain HTTP when zerotrust:false
staticpaths: [site]
domainmaps:
- name: local
domains: [localhost, 127.0.0.1]
customer: kisai
product: spahttp
environment: dev
tenant: local

Plain static site (Hugo / MkDocs / Docusaurus)

Section titled “Plain static site (Hugo / MkDocs / Docusaurus)”

Use type: static and point notfoundpath at your 404.htmlgateway.svc config generate static-site.

The spawned fileserver binds to the services[].url port on localhost. The gateway proxies the route to it; the route’s middleware chain (auth, mapper, metrics, …) runs in the gateway, in front of the fileserver.

URL rewrites, redirects & header transforms

Section titled “URL rewrites, redirects & header transforms”

These are endpoint-level blocks (rewrite:, redirect:, headers:, security_headers:) — gateway.svc config generate url-rewrite.

endpoints:
# strip a prefix: /api/v1/users → /users
- name: strip
path: { prefix: /api/v1 }
backend: api-svc
rewrite:
strip_prefix: /api/v1
# strip then add: /api/v1/users → /internal/users
- name: combo
path: { prefix: /api/v1 }
backend: internal-svc
rewrite:
strip_prefix: /api/v1
add_prefix: /internal
# regex with capture groups: /v1/users/42 → /v2/u/42
- name: regex
path: { prefix: /v1/users }
backend: users-svc
rewrite:
path:
match: ^/v1/users/(\d+)$
replace: /v2/u/$1
# rewrite the Host header the backend sees
- name: host
path: { prefix: /vendor }
backend: vendor-svc
rewrite:
strip_prefix: /vendor
host: vendor.partner.com

rewrite.path (static or {match, replace}) cannot be combined with strip_prefix/add_prefix on the same endpoint.

Redirect (no backend — terminates the chain)

Section titled “Redirect (no backend — terminates the chain)”
endpoints:
- name: legacy
path: { prefix: /old }
redirect:
from: ^/old/(.*)$ # default ^.*$
to: /new/$1
permanent: true # 308 (true) vs 302 (false)

redirect: is mutually exclusive with backend: and with rewrite:.

endpoints:
- name: headers
path: { prefix: /augmented }
backend: app-svc
headers:
request:
set:
X-Api-Version: "v1"
X-Source: gateway
remove: [Cookie, Authorization]
response:
set:
Cache-Control: "no-store"
remove: [Server, X-Powered-By]
endpoints:
- name: secure
path: { prefix: /app }
backend: app-svc
security_headers: strict # strict | relaxed | none
  • strict — HSTS (2y, preload), frameDeny, nosniff, XSS filter, Referrer-Policy, CSP default-src 'self'.
  • relaxed — HSTS (1y), X-Frame-Options: SAMEORIGIN, nosniff, XSS filter, Referrer-Policy (no CSP).
  • none — emit nothing.

Header values accept ${...} template substitution, resolved per request from a curated namespace — gateway.svc config generate headers-expr. The values are populated by upstream middlewares (customer-domain-mapper, kisaiauth), so put the mapper before the header block.

endpoints:
- name: tenant-fwd
path: { prefix: /api }
backend: api-svc
middlewares:
- customer-domain-mapper: { default: true }
headers:
request:
set:
X-Tenant-Id: "${tenant.customer}"
X-Tenant-Slug: "${tenant.customer}-${tenant.env}"
X-User: "${auth.subject}"
remove: [Authorization]
response:
set:
X-Served-By: "gateway.${gateway.cluster}"
X-Backend-Time: "${upstream.duration_ms}ms"

Reserved namespaces:

NamespaceFields
request.*method, scheme, host, path, path_segments[N], query.<n>, headers.<n>, cookies.<n>, remote_addr, body_size
tenant.*customer, product, env, name
auth.*subject, user_id, realm
response.*status, headers.<n> (response-side only)
upstream.*duration_ms (response-side only)
gateway.*datacenter, cluster, version

A missing variable renders as the empty string; for a header value, empty means “strip the header”. CRLF in rendered values is stripped (request-smuggling defence).

Declare named profiles once; backends opt in via profile:. Backends with more than one URL round-robin by default — gateway.svc config generate load-balancing.

loadbalance:
default: # applied to any backend without an explicit profile
algo: wrr
healthcheck: { path: /health, interval: 30s, timeout: 5s }
profiles:
sticky-session: # cookie-based session affinity
algo: sticky
sticky: { cookie: srv-id, httpOnly: true, secure: true }
weighted-80-20: # static traffic split across backends
algo: weighted
members:
- { backend: prod, weight: 4 }
- { backend: canary, weight: 1 }
canary-10pct: # shadow 10% to a canary (fire-and-forget)
algo: mirror
primary: prod
mirrors: [{ backend: canary, percent: 10 }]
region-failover: # primary + hot standby; health-check drives the flip
algo: failover
primary: us-east
fallback: us-west
healthcheck: { path: /health/deep, interval: 10s, timeout: 3s }
adaptive-cpu-mem: # gateway polls each member and rewrites weights
algo: adaptive
members: [{ backend: pool-a }, { backend: pool-b }, { backend: pool-c }]
reconcile: { interval: 10s, metrics_path: /health/load, timeout: 3s }
formula: cpu_then_memory # cpu_then_memory | inflight_count | available_capacity
min_weight: 1
max_weight: 100
AlgorithmUse
wrrWeighted round-robin (the default).
stickyWRR + cookie session affinity.
weightedStatic split across multiple backends (canary by share).
mirrorPrimary serves; mirrors get a sampled copy, responses discarded.
failoverPrimary with a health-gated fallback.
adaptiveGateway polls each member’s metrics_path every interval and recomputes weights from formula.

For adaptive, each member must expose a JSON endpoint at metrics_path (default /health/load) with CPU/memory/inflight fields. reconcile.interval has a 5s floor. The controller keeps last-known weights after transient probe failures.

Two independent timeout layers can cut a request, and both live under timeouts: in the bootstrap .gateway.yaml. timeouts is a boot-plane key, so it is stripped from routing-dir files — putting it in common.yaml/backend.yaml does nothing (you’ll get a warning in the log).

# .gateway.yaml — these are the defaults
timeouts:
read: 10s # max time to read the request (headers + body)
write: 0s # max time to write the WHOLE response; 0 = no deadline (default)
idle: 180s # max keep-alive idle between requests on a connection

These become Traefik’s respondingTimeouts and apply to every route on the entrypoint.

write defaults to 0s (no deadline), so SSE / WebSockets / streaming work out of the box. If you set a positive write for slow-write protection, know that it’s a hard deadline on the entire response, not an idle timer — it severs any stream at exactly write seconds no matter how much data is flowing (a source emitting an event every 3s is still cut at the 10s mark; heartbeats do not reset it, and Traefik does not exempt streaming). So either leave it 0s, or set it and route streaming endpoints through a dedicated entrypoint that keeps write: 0s (below). read and idle still bound request reads and idle keep-alives regardless.

To keep the cap on normal routes and lift it only for streams, declare an explicit entrypoints: block (gateway.svc config generate multi-port) and override timeouts on the listener the streaming routes use:

entrypoints:
- name: stream
address: ":8446"
protocol: http
timeouts: { write: 0s } # only this listener drops the write deadline

A second layer sets the request-handling context deadline per backend, with per-path regex overrides — also under timeouts: in the bootstrap (or delivered via the remote cluster config from config.svc):

.gateway.yaml
timeouts:
services:
- name: app.orders # the service (backend) name
defaultcontexttimeout: 30s # default context deadline for this service
pathconfig:
- { expr: "^/orders/export/.*", duration: 120s } # slower path, longer deadline
- { expr: "^/orders/stream/.*", duration: 0s } # 0s = no deadline (streaming)

pathconfig entries are matched by expr (Go regexp) against the request path, first match wins. duration: 0s (or enabled: false) removes the deadline for that path — use it for streaming endpoints.

Reach for the per-service layer only when you need different deadlines per backend or path. For a plain “my SSE dies after 10s”, the listener write: 0s is the whole fix.

External TLS and internal mTLS are two independent planes — don’t conflate them:

  • External (public) TLS — client ↔ gateway on a public entrypoint. A route opts in by declaring certresolver: (a Let’s Encrypt / ACME cert) and/or tlsoptions: / tlspassthrough:. This is independent of zerotrust — a route can serve public HTTPS with an LE cert whether or not the internal mesh is on. Works on both endpoints: and staticpaths: routes.
  • Internal mTLSzerotrust: true turns on mutual TLS on the websecureinternal entrypoint and the gateway→backend leg (via kisaitlsinternal / the kisai_issuer CA). That’s service-mesh identity, not public serving; you do not need it to get a public HTTPS cert.

Hardened TLS profiles live in tlsoptions (usually in a shared common.yamlgateway.svc config generate common):

tlsoptions:
- default:
minVersion: VersionTLS12
sniStrict: true
cipherSuites: [TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, ]
- kisaitlsinternal:
minVersion: VersionTLS12
clientAuth:
caFiles: [/etc/gateway/certs/kisai_issuer.crt]
clientAuthType: RequireAndVerifyClientCert

default and kisaitlsinternal are protected names — you may add new ones but not redefine these. A route selects one with tlsoptions: <name> (optional; a route with only certresolver: uses the default options automatically).

A public Let’s Encrypt cert, step by step

Section titled “A public Let’s Encrypt cert, step by step”
  1. Declare the resolver + the :80 challenge entrypoint once in the traefik: passthrough (in common.yaml):

    traefik:
    static:
    certificatesResolvers:
    letsencrypt:
    acme:
    storage: /var/lib/gateway/acme.json # writable + persisted across restarts
    httpChallenge: { entryPoint: web }
    entryPoints:
    web:
    address: ":80"
    http:
    redirections:
    entryPoint: { to: websecure, scheme: https, permanent: true }
  2. Point the route at it — no zerotrust required:

    routes:
    - name: main
    domainmap: main
    entrypoint: websecure
    certresolver: letsencrypt
    staticpaths: [build] # or endpoints: [...]
    domainmaps:
    - { name: main, domains: [app.kisai.dev], customer: kisai, product: app, environment: prod, tenant: main }
  3. Issuance requires (all of them, or the challenge fails and the gateway serves a self-signed cert):

    • the route’s domains: publicly resolve to this host;
    • the challenge port is reachable from the internet — :80 for HTTP-01 (the webwebsecure redirect does not interfere: Traefik serves /.well-known/acme-challenge/ before redirecting);
    • the storage: (acme.json) path is writable and persisted.
  • HTTP-01 (gateway.svc config generate acme-http01) — simplest; needs public DNS A records and inbound :80.
  • DNS-01 (gateway.svc config generate acme-dns01) — required for wildcards or behind a CDN. DNS-API credentials go in env vars, never YAML.

Troubleshooting — the browser shows a self-signed / “not trusted” cert. That means no LE cert was obtained and the gateway is serving its default-store cert. Check, in order:

  1. the route actually declares certresolver: and the running binary emits itgrep certResolver in the debug log should show tls.certResolver on your router (if absent, rebuild/redeploy the binary);
  2. an ACME attempt is logged for your domain (grep -i 'acme\|obtain') — if there’s none, the router carries no resolver;
  3. the challenge port (:80) is reachable from outside (curl http://<domain>/.well-known/acme-challenge/probe from another host — connection refused/timeout ⇒ firewall);
  4. acme.json is writable.

For Traefik features the gateway doesn’t surface as first-class fields (ACME, dashboard, custom middlewares, http3, …), drop raw Traefik config under traefik.static / traefik.dynamic — by convention, only in common.yaml.

traefik:
static:
certificatesResolvers:
letsencrypt:
acme:
storage: /var/lib/gateway/acme.json
httpChallenge: { entryPoint: web }
dynamic:
http:
middlewares:
strict-rate-limit: { rateLimit: { average: 100, burst: 200 } }
gzip-all: { compress: {} }

Reference them from a route:

routes:
- name: main
domainmap: main
entrypoint: websecure
certresolver: letsencrypt # → traefik.static.certificatesResolvers
extramiddlewares: [strict-rate-limit, gzip-all] # → traefik.dynamic.http.middlewares
endpoints: [to-svc]

Protected paths (the gateway owns these; writes are rejected at boot):

  • traefik.static: entryPoints.websecure, entryPoints.websecureinternal, providers.kisai, global, log, accessLog.filePath, experimental.plugins/localPlugins, serversTransport.insecureSkipVerify/rootCAs, api.insecure.
  • traefik.dynamic: http.routers, tcp.routers, http.services, tcp.services, tls.options.default, tls.options.kisaitlsinternal.

The Traefik dashboard can be exposed behind kisai-authgateway.svc config generate dashboard (never set api.insecure: true).

kisaimetrics:
enabled: true
ipaddr: 127.0.0.1
port: 7011 # default 8088

Serves Prometheus metrics at /metrics and Go profiling at /debug/pprof/* on its own listener.

tracked:
cookiename: kisaitracker
expiry: 2160h # 90 days
anonymous:
enabled: true
publishhostport: localhost:8000
publishuri: /account/user/anonuser
excludedpaths: [/health, "/\\.well-known/.*"]
accesslogs:
enabled: true
publishhostport: localhost:8000
publishuri: /telemetry/access

The tracker middleware assigns an anonymous cookie and publishes new-tracker and access-log events asynchronously (fire-and-forget); it is disabled on internal (websecureinternal) traffic. Non-fatal if the publish endpoint is unset.

When config.router.dir points at a folder, every *.yaml in it is merged:

  • List keys (routes, domainmaps, backends, endpoints, staticpaths, tlsoptions) concatenate across files.
  • Everything else is last-wins.

Each file declares a namespace:; names in that file (and bare same-file references) get prefixed with <namespace>., so two files can both define a main route without colliding. Qualified references (common.to-config) cross files and are left as-is. tlsoptions are never namespaced — define them once in common.yaml and reference by plain name.

config/forge.yaml
namespace: forge
routes:
- name: tenant-a # → forge.tenant-a
domainmap: tenant-a # → forge.tenant-a (same-file, auto-prefixed)
entrypoint: websecure
endpoints:
- common.to-config # qualified → left as-is
- to-arkabot # bare → forge.to-arkabot
tlsoptions: kisaitlsinternal

The loader fails fast at boot on: duplicate names in a list; a list key with a non-list value; unresolved references (route→domainmap/endpoint/staticpath, endpoint→backend); an unknown certresolver; or a write to a protected passthrough path. A misconfigured deploy never silently serves half its routes. See config.example/ for a complete multi-file layout.

gateway.svc [flags] # run the gateway
gateway.svc config list # list embedded config examples
gateway.svc config generate <name> # print an example (or -d <dir> to write it)
gateway.svc config generate all -d <dir> # write every example
gateway.svc version # print the build version

Flags (all also settable in the bootstrap file where applicable):

FlagMeaning
-f, --config <file>Bootstrap file (default .gateway.yaml)
-l, --logstdout or host:port
-L, --loglevelinfo|debug|warn|error
-p, --portOverride the public port
--datacenter, --clusterDeployment tags (default default)
--config_url, --config_apikeyRemote config service (folds into the config: block)
--servicediscovery_url, --servicediscovery_apikeyService-discovery registration
--svccert, --svckey, --rootcaTLS material for zero-trust
--zerotrusttrue|false
--jwt_public_keyJWT verification key path
--sidService instance id

See Operations for bootstrap, hot reload, observability, and failure modes.

Stable: backend routing, static & SPA serving, path rewrites / redirects / header transforms, the ${...} expression engine, security-header presets, load balancing (wrr/sticky/weighted/mirror/failover/adaptive), metrics, anonymous-tracker & access-log telemetry, ACME (HTTP-01 / DNS-01), Traefik passthrough, multi-file config + hot reload, HTTP entrypoints.

Partial / experimental: TCP/UDP (L4) entrypoints — L4 traffic bypasses the HTTP middleware chain, so there is no per-request tenant attribution (declare one tenant per L4 port); UDP service wiring is incomplete.

Planned (not yet implemented): scheduled/time-ramp rollouts, SLO-aware adaptive weights (error-rate / latency formulas), traffic variants (header/cookie/bucket cohorts), and Tier-2 compute: expressions.