Gateway 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 CEPT (tenant) resolution, header/path
transforms, and metric/telemetry middleware on top.
This guide is the quickstart and the how-to. Every example named below is
embedded in the binary, and gateway.svc config generate <name> prints it.
Quick start
Section titled “Quick start”Local dev, HTTP-only, no certs, runs without root:
# 1. Bootstrap file (listeners, log level, where routing lives)gateway.svc config generate quickstart > .gateway.yaml
# 2. A routing config, dropped into the config/ directorygateway.svc config generate spa-http -d config/
# 3. Content for the SPA example to servemkdir -p public && echo '<h1>hello gateway</h1>' > public/index.html
# 4. Run itgateway.svc -f .gateway.yaml -L debug
# 5. Hit itcurl http://localhost:8443/See what else ships:
gateway.svc config list # every embedded example + one-linergateway.svc config generate single-app # print one to stdoutgateway.svc config generate common -d config/ # write config/common.yamlgateway.svc config generate all -d config/ # write every exampleFor production, start from bootstrap instead of quickstart (port 443,
zerotrust: true).
How config is loaded
Section titled “How config is loaded”The gateway reads two kinds of configuration, and they are kept separate:
| Kind | What it is | Where it comes from |
|---|---|---|
| Boot / service config | Listeners, log level, TLS posture, datacenter/cluster, and the pointer to the routing config | The -f bootstrap file, plus the chassis config: block (local folder or remote config.svc) |
| Routing config | domainmaps, backends, endpoints, routes, staticpaths. The gateway’s own data model | Inline 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.svcserves the service config and the routing model (the same top-leveldomainmaps/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
-ffile is the whole config. (This is what most of the embedded single-file examples do.)
Note,
config.router.dirvsconfig.path.config.pathis the chassis’s service-config folder (lib-config flat-pathformat). The gateway’s routing directory is a different format and uses its own key,config.router.dir. Don’t put routing files underconfig.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.
Boot keys (top level of the bootstrap)
Section titled “Boot keys (top level of the bootstrap)”name: API Gateway # service name (optional)log: stdout # stdout | host:port (fluentd)loglevel: info # info | debug | warn | error
host: "" # bind address; "" = all interfacesport: 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: 180sThe mental model
Section titled “The mental model”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)domainmapsmap one or moreHostvalues to a CEPT tuple. Thecustomer-domain-mappermiddleware resolves the incoming host and injectsX-Customer,X-Product,X-Env,X-Tenantheaders so backends know which tenant a request belongs to.backendsare named pools of upstream URLs.endpointsmatch a path prefix and forward to a backend, applying an ordered middleware chain.routestie adomainmap+ anentrypointto a set ofendpointsand/orstaticpaths.entrypointsare the listeners. With no explicitentrypoints:block the gateway synthesises two from the boot keys:websecure(:{port}) andwebsecureinternal(:{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 honoredBackend routing
Section titled “Backend routing”One app on one domain
Section titled “One app on one domain”# gateway.svc config generate single-appbackends: - 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: mainEvery 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.)
Multi-tenant (one product, many tenants)
Section titled “Multi-tenant (one product, many tenants)”Give each tenant its own subdomain and domainmap; share the backends and
endpoints:
# gateway.svc config generate multi-tenantroutes: - 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-bThe same backend pool serves both tenants; the customer-domain-mapper stamps
the right X-Tenant per request so the backend can isolate data.
Backend options
Section titled “Backend options”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)Static sites & SPAs
Section titled “Static sites & SPAs”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:
type | Behaviour |
|---|---|
static | Serves files as-is. Missing file → notfoundpath (with 404) if set, else plain 404. |
spa | Same, 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. |
SPA (React / Vue / Svelte) over HTTPS
Section titled “SPA (React / Vue / Svelte) over HTTPS”# gateway.svc config generate spastaticpaths: - 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 endpointsBuild 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.
Self-contained SPA over HTTP (local dev)
Section titled “Self-contained SPA over HTTP (local dev)”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-httpstaticpaths: - 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: localPlain static site (Hugo / MkDocs / Docusaurus)
Section titled “Plain static site (Hugo / MkDocs / Docusaurus)”Use type: static and point notfoundpath at your 404.html,
gateway.svc config generate static-site.
The spawned fileserver binds to the
services[].urlport 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.
Rewrite (path + host)
Section titled “Rewrite (path + host)”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.comrewrite.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:.
Header set/remove
Section titled “Header set/remove”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]Security-header presets
Section titled “Security-header presets”endpoints: - name: secure path: { prefix: /app } backend: app-svc security_headers: strict # strict | relaxed | nonestrict: HSTS (2y, preload),frameDeny, nosniff, XSS filter,Referrer-Policy, CSPdefault-src 'self'.relaxed: HSTS (1y),X-Frame-Options: SAMEORIGIN, nosniff, XSS filter,Referrer-Policy(no CSP).none: emit nothing.
Expression engine
Section titled “Expression engine”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:
| Namespace | Fields |
|---|---|
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).
Load balancing
Section titled “Load balancing”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| Algorithm | Use |
|---|---|
wrr | Weighted round-robin (the default). |
sticky | WRR + cookie session affinity. |
weighted | Static split across multiple backends (canary by share). |
mirror | Primary serves; mirrors get a sampled copy, responses discarded. |
failover | Primary with a health-gated fallback. |
adaptive | Gateway 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.
Timeouts & streaming
Section titled “Timeouts & streaming”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).
Listener timeouts (per connection)
Section titled “Listener timeouts (per connection)”# .gateway.yaml — these are the defaultstimeouts: 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 connectionThese become Traefik’s respondingTimeouts and apply to every route on the
entrypoint.
writedefaults to0s(no deadline), so SSE / WebSockets / streaming work out of the box. If you set a positivewritefor slow-write protection, know that it’s a hard deadline on the entire response, not an idle timer, it severs any stream at exactlywriteseconds 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 it0s, or set it and route streaming endpoints through a dedicated entrypoint that keepswrite: 0s(below).readandidlestill 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 deadlinePer-service request-context timeouts
Section titled “Per-service request-context timeouts”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):
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.
TLS & ACME
Section titled “TLS & ACME”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/ortlsoptions:/tlspassthrough:. This is independent ofzerotrust: a route can serve public HTTPS with an LE cert whether or not the internal mesh is on. Works on bothendpoints:andstaticpaths:routes. - Internal mTLS:
zerotrust: trueturns on mutual TLS on thewebsecureinternalentrypoint and the gateway→backend leg (viakisaitlsinternal/ thekisai_issuerCA). 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.yaml,
gateway.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: RequireAndVerifyClientCertdefault 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”-
Declare the resolver + the
:80challenge entrypoint once in thetraefik:passthrough (incommon.yaml):traefik:static:certificatesResolvers:letsencrypt:acme:storage: /var/lib/gateway/acme.json # writable + persisted across restartshttpChallenge: { entryPoint: web }entryPoints:web:address: ":80"http:redirections:entryPoint: { to: websecure, scheme: https, permanent: true } -
Point the route at it: no
zerotrustrequired:routes:- name: maindomainmap: mainentrypoint: websecurecertresolver: letsencryptstaticpaths: [build] # or endpoints: [...]domainmaps:- { name: main, domains: [app.kisai.dev], customer: kisai, product: app, environment: prod, tenant: main } -
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:
:80for HTTP-01 (theweb→websecureredirect does not interfere: Traefik serves/.well-known/acme-challenge/before redirecting); - the
storage:(acme.json) path is writable and persisted.
- the route’s
- 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:
- the route actually declares
certresolver:and the running binary emits it ,grep certResolverin the debug log should showtls.certResolveron your router (if absent, rebuild/redeploy the binary);- an ACME attempt is logged for your domain (
grep -i 'acme\|obtain'): if there’s none, the router carries no resolver;- the challenge port (
:80) is reachable from outside (curl http://<domain>/.well-known/acme-challenge/probefrom another host, connection refused/timeout ⇒ firewall);acme.jsonis writable.
Traefik passthrough
Section titled “Traefik passthrough”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-auth,
gateway.svc config generate dashboard (never set api.insecure: true).
Metrics & telemetry
Section titled “Metrics & telemetry”kisaimetrics: enabled: true ipaddr: 127.0.0.1 port: 7011 # default 8088Serves 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/accessThe 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.
Multi-file config directory
Section titled “Multi-file config directory”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.
namespace: forgeroutes: - 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: kisaitlsinternalThe 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.
CLI reference
Section titled “CLI reference”gateway.svc [flags] # run the gatewaygateway.svc config list # list embedded config examplesgateway.svc config generate <name> # print an example (or -d <dir> to write it)gateway.svc config generate all -d <dir> # write every examplegateway.svc version # print the build versionFlags (all also settable in the bootstrap file where applicable):
| Flag | Meaning |
|---|---|
-f, --config <file> | Bootstrap file (default .gateway.yaml) |
-l, --log | stdout or host:port |
-L, --loglevel | info|debug|warn|error |
-p, --port | Override the public port |
--datacenter, --cluster | Deployment tags (default default) |
--config_url, --config_apikey | Remote config service (folds into the config: block) |
--servicediscovery_url, --servicediscovery_apikey | Service-discovery registration |
--svccert, --svckey, --rootca | TLS material for zero-trust |
--zerotrust | true|false |
--jwt_public_key | JWT verification key path |
--sid | Service instance id |
Operations
Section titled “Operations”See Operations for bootstrap, hot reload, observability, and failure modes.
See also
Section titled “See also”- Entrypoints: listeners, TCP and UDP, per-listener timeouts
- Load balancing: profiles in full, including adaptive weights
- Passthrough: raw Traefik configuration and the protected paths
- Security: the edge posture and a review checklist