SERVE FLAGS
Flags
| Flag | Default | Description |
|---|---|---|
--config <path> |
Path to a YAML config file. Default: .swamp/serve.yaml (loaded silently if present). See Config file |
|
--port <port> |
9090 |
Port to listen on |
--host <host> |
127.0.0.1 |
Host to bind to |
--cert-file <path> |
Path to PEM-encoded TLS certificate | |
--key-file <path> |
Path to PEM-encoded TLS private key | |
--auth-mode <mode> |
none |
Authentication mode: none (deprecated), token, or oauth. The mode governs clients connecting to Serve. The Serve process itself needs a swamp-club.com credential in every mode; token and oauth additionally need it to carry the serve:* scope |
--club-api-key-file <path> |
Path to a file holding the collective API key. Takes precedence over SWAMP_API_KEY_FILE and SWAMP_API_KEY, and the same key is used for the whole process: OAuth client registration, admin and allowed-user name lookup, instance registration, the club heartbeat, extension trust and pulls, and telemetry. Read once at startup for registration, name lookup and the heartbeat, so a rotated file needs a restart. Also accepted by swamp serve check-config; swamp serve daemon enable stores only the absolute path. See API key authentication |
|
--admins <principals> |
Comma-separated principal IDs for admin access | |
--grant-reload <mode> |
manual |
Policy snapshot reload: manual or auto |
--grants-file <path> |
Path to an external grants YAML file loaded at startup. Grants from this file are merged with any grants in the grants/ directory. A relative path resolves against the repository directory, so the same configuration loads the same grants wherever swamp serve, or the daemon, is started from |
|
--remote-only |
(off) | Disable local (loopback) execution — all workflow steps must declare placement (target, labels, or platform) and be dispatched to remote workers. Steps without placement error immediately. See Remote-only mode |
--no-schedule |
Disable scheduled workflow execution | |
--webhook <spec> |
Register a webhook endpoint (repeatable) | |
--allowed-collectives |
Comma-separated collective slugs for OAuth admission | |
--allowed-users |
Comma-separated user identifiers for OAuth admission | |
--oauth-provider <url> |
OAuth authorization server URL | |
--oauth-client-id <id> |
OAuth client ID (env: SWAMP_OAUTH_CLIENT_ID). When set, serve never registers a client and needs the client's secret from --oauth-client-secret-file, SWAMP_OAUTH_CLIENT_SECRET_FILE, SWAMP_OAUTH_CLIENT_SECRET or a usable stored copy, or it refuses to start. When omitted, a client is registered on first start |
|
--oauth-client-secret-file <path> |
Path to a file holding the secret of the client named by --oauth-client-id. Used for this run only and never stored. Overrides SWAMP_OAUTH_CLIENT_SECRET_FILE and SWAMP_OAUTH_CLIENT_SECRET. Refused without a client ID. Requires Swamp v20261103.225037.0-sha.5911dc9c or later |
|
--keep-oauth-client-in-vault |
(off) | Leave oauth-client-id and oauth-client-secret in the user vault when moving OAuth secrets into the control-plane store (env: SWAMP_KEEP_OAUTH_CLIENT_IN_VAULT). Off by default: anything that can read that vault can read the client secret. Requires Swamp v20261103.225037.0-sha.5911dc9c or later |
--oauth-client-name <name> |
Override the OAuth client_name used during dynamic client registration. Default: swamp-serve-{repoName}-{hostname} (truncated to 128 chars). Useful in container/k8s environments where hostname is ephemeral |
|
--groups-field <field> |
Userinfo field name the collectives are read from (default: collectives) |
|
--trust-proxy |
Trust a reverse proxy's forwarding headers: the client IP, used for rate limiting, is read from the leftmost X-Forwarded-For entry instead of the connection's address, and dashboard sign-in takes the public scheme and host from X-Forwarded-Proto and X-Forwarded-Host |
|
--ws-idle-timeout <duration> |
WebSocket idle timeout — how long the server waits for a pong before closing the connection. Accepts seconds (30), explicit units (2m, 5m), or 0 to disable |
|
--queue-timeout <duration> |
10m |
How long a placed step queues for a matching worker before timing out. Accepts seconds (60), explicit units (2m, 10m), or 0 to disable |
--verify-on-enroll |
(off) | Run a fleet probe on each enrolling worker before it becomes schedulable. Workers that fail the probe are marked unverified and excluded from dispatch until they re-enroll successfully |
--dispatch-env-allow <names> |
Comma-separated environment variable names. Only these ship to workers with each dispatch. Unset ships serve's whole environment minus a fixed denylist. See Dispatch environment | |
--hot-reload |
(off) | Enable SIGHUP-based hot-reload for pulled extension bundles and the trusted collectives list. Writes a PID file to .swamp/serve.pid; use swamp serve reload to trigger a reload |
--group-refresh-interval <d> |
4h |
How often the server refreshes IdP group memberships from the OAuth provider's userinfo endpoint. Accepts duration units (30m, 4h, 1d), or 0 to disable |
--restricted-model-types <types> |
Comma-separated behavior types whose behaviors require admin on access:* to create, run, edit, delete, delete or rename data of, or reference from a changed workflow step. Non-admin users are denied with Access denied: <principal> does not have 'admin' on access:*. Types are normalized on parse — Command::Shell, COMMAND.SHELL, and command/shell all match — and a leading @ is ignored, so @acme/deploy and acme/deploy restrict the same type. An entry that names no type stops serve from starting. See Restricted behavior types |
|
--restricted-commands <cmds> |
Comma-separated server commands that require admin authority (e.g. datastore.namespace.list,extension.install). Non-admin users are denied with Access denied: <principal> does not have 'admin' on access:<command>. Names match exactly; serve warns at startup about an entry that is not a server command. Requires --auth-mode token or oauth |
|
--approve-requires-explicit-grant |
(off) | Require a grant that names approve to decide a manual approval gate; an allow grant of run alone no longer implies approve. A deny grant of run still denies approve. Every replica must use the same value. Requires --auth-mode token or oauth. See Run implies approve |
--trusted-hosts <hosts> |
Comma-separated hostnames or host:port entries to trust for Host header and browser Origin validation when binding off-loopback or serving through a reverse proxy (e.g. host.docker.internal,host.minikube.internal). A public address on a port other than 443 is given as host:port |
|
--advertise-address <url> |
This node's own http(s) origin, as the other serve nodes sharing its v3 datastore reach it (e.g. https://node-1.internal:9090). The leader record names it, and followers forward writes to the leader at this address; without it a node bound to a wildcard or loopback host cannot be forwarded to. Refused with a path, query, credentials or wildcard host. A plain http:// address to a host that is not loopback starts with a warning. See Run a Multi-Instance Deployment |
|
--ca-cert <path> |
PEM file of CA certificates to trust, besides the system store, when this node forwards to its team's leader. A relative path resolves against the repository. Serve reads the file at startup and refuses to start when it cannot | |
--detach-runs |
(off) | Deprecated. Accepted for backwards compatibility but has no effect. HA mode is now auto-detected based on the datastore configuration — when the datastore supports a control plane (e.g. S3), runs are automatically detached and reconciliation is enabled |
--heartbeat-interval <duration> |
30s |
How often to write an instance heartbeat to the control-plane store. Accepts duration units (15s, 30s, 1m). Only effective with a control-plane-capable datastore |
--stale-ttl <duration> |
90s |
How long a heartbeat can go without update before the instance is considered dead. Must be at least 2× --heartbeat-interval. Accepts duration units (60s, 90s, 3m). Only effective with a control-plane-capable datastore |
--reconciliation-interval <duration> |
60s |
How often to scan for dead peer instances and reconcile their orphaned runs. Accepts duration units (30s, 60s, 2m). Only effective with a control-plane-capable datastore |
--hydration-timeout <duration> |
60s |
Maximum time to wait for initial datastore cache hydration at startup. Accepts seconds (60), explicit units (60s, 5m). Increase for large repos where the initial pull takes longer |
--datastore-poll-interval <duration> |
30s |
How often to pull config, access data (grants and groups) and runtime data from a remote datastore, and how often serve checks a v2 filesystem datastore for grants and groups changed by another process. This is how stale a peer instance can be: lower values converge instances faster at the cost of more list and get calls against the bucket. Minimum 1s. Accepts seconds (30) or units (30s, 1m, 1h); milliseconds are rejected. See Datastore polling |
--token-gc-interval <duration> |
1h |
How often to garbage-collect server tokens. Accepts seconds (3600) or units (30s, 1h, 1d), or 0 to disable. See Token garbage collection |
--token-gc-grace-period <duration> |
1h |
How long after its expiry an expired token is kept before it is collected. Accepts seconds (3600) or units (30s, 1h, 1d), or 0 to collect a token as soon as it expires |
--max-concurrent-runs <n> |
100 |
Maximum number of concurrent detached runs across all principals |
--max-concurrent-scheduled-runs <n> |
1 |
Maximum number of scheduled workflow runs in flight at once. Different workflows run together up to this limit; a workflow never overlaps itself. Separate from --max-concurrent-runs, which limits API runs. Read at startup, not on reload. See Run Workflows on a Schedule |
--max-runs-per-principal <n> |
Maximum number of concurrent detached runs per authenticated principal. Unset by default (no per-principal limit) | |
--max-run-duration <duration> |
Maximum wall-clock time a detached run may execute before being aborted. Accepts seconds (3600), explicit units (1h, 30m). Unset by default (no limit) |
|
--grants-dir <path> |
Path to a directory of grants YAML files loaded at startup. A relative path resolves against the repository directory, so the same configuration loads the same grants wherever swamp serve, or the daemon, is started from |
|
--dashboard |
(off) | Enable the web dashboard at /dashboard. The dashboard is opt-in and adds no overhead when disabled. See Dashboard |
--auto-resume |
(off) | Resume a suspended run once an approval made through serve decides its last gate. Applies to workflows that declare no inputs and leave autoResume unset; a workflow's own autoResume takes precedence. A local swamp workflow approve never triggers it. See Auto-resume |
--enable-internal-api |
(off) | Enable the /internal/runs endpoint for full run history access |
Duration flags that drive a timer (--heartbeat-interval,
--reconciliation-interval, --group-refresh-interval, --hydration-timeout,
--datastore-poll-interval, --token-gc-interval and --max-run-duration)
reject values above 2147483647 ms, about 24.8 days:
Error: --token-gc-interval (30d) exceeds the maximum safe timer duration (~24.8 days)Off-loopback binding (--host set to anything other than 127.0.0.1 or ::1)
requires both TLS (--cert-file + --key-file) and authentication
(--auth-mode other than none, which is itself deprecated). The server
refuses to start without them. By default, Host header validation rejects
requests whose Host does not match the bound address — a DNS rebinding defense.
Use --trusted-hosts to whitelist additional hostnames (such as
host.docker.internal) so Docker and Kubernetes workers can connect via
platform-specific hostnames without disabling the protection.
A --trusted-hosts entry is a hostname or host:port. Each entry applies to
three checks:
| Check | Bare hostname | host:port |
|---|---|---|
| Host header (DNS rebinding) | Trusts the hostname | Trusts the hostname; port ignored |
| Browser Origin, for WebSocket upgrades and no-auth HTTP APIs | Trusts the host on any port | Trusts only that exact origin |
| Dashboard sign-in | Trusts https://<host> (port 443) |
Trusts https://<host>:<port> |
Dashboard sign-in needs no entry when the browser origin matches the scheme and
authority of the request serve receives (with --trust-proxy, the forwarded
scheme and host). An entry is for a proxy that rewrites them.
When a proxy publishes the dashboard on a port other than 443, list the public
address as host:port. For example, behind tailscale serve --https=9443 the
browser origin is https://<public-host>:9443, and the one entry
<public-host>:9443 passes both the WebSocket and dashboard sign-in checks; a
bare <public-host> passes the WebSocket check but not sign-in.
Dispatch environment
Serve ships a snapshot of its environment with every step it dispatches to a
worker. dispatch-env-allow limits the snapshot to the names it lists.
| Setting | Ships |
|---|---|
| Unset (default) | Serve's whole environment, minus the denylist |
| A list of names | Only the listed names, minus the denylist |
dispatch-env-allow: [] |
Nothing |
SWAMP_DISPATCH_ENV_ALLOW empty |
Nothing |
Names match case-insensitively.
The denylist always applies:
HOME,PATH,SWAMP_*,DENO_*,XDG_*,OTEL_*and the other process-identity and host-runtime variables never ship.The flag needs a value. To ship nothing, use the config file or the empty environment variable.
In
serve.yaml, a blank entry or an entry containing a comma stops serve from starting:Error: Invalid dispatch-env-allow in /srv/repo/.swamp/serve.yaml: expected array of environment variable names
At startup, serve logs the scope and warns about any listed name the denylist drops:
system │ dispatch-env-allow: only these environment variables ship to workers: "DEPLOY_ENV, AWS_REGION, HOME"
system │ dispatch-env-allow lists "HOME", which never ships to workersA name left off the list keeps the worker's own value. See Remote Execution.
Resource limits
At startup, swamp serve automatically raises the process's open-file soft
limit to 8192 using setrlimit. Each connected client, worker WebSocket, and
datastore handle consumes a file descriptor, so the stock macOS soft limit of
256 is too low for most server workloads.
| Condition | Behavior |
|---|---|
| Hard limit ≥ 8192 (stock macOS: unlimited) | Soft limit raised to 8192 automatically; an info-level log confirms the raise |
| Hard limit < 8192 | Raise skipped; a warning is logged with instructions to raise the limit before starting |
No user action is needed on stock macOS, where the soft limit is 256 and the hard limit is unlimited — the server fixes it automatically. When the hard limit is also restricted (some container runtimes and hardened Linux hosts), raise it before starting the server:
# Shell session (applies to the current shell and its children)
ulimit -Sn 8192
# systemd unit (permanent)
# Add to the [Service] section of the unit file:
LimitNOFILE=8192Health endpoint
GET /ready returns a JSON object indicating whether the server is ready to
accept requests. The response includes an instanceId field — a unique
identifier for this server instance, stable for the lifetime of the process:
{ "ready": true, "instanceId": "a1b2c3d4" }The instanceId is used internally for heartbeat registration and run
ownership. In a multi-instance deployment, each instance reports a distinct
instanceId, which can help identify which instance is handling a request or
owns a particular run.
Auto-resume
A run that passes a manual_approval gate stays suspended until it is resumed.
With --auto-resume, serve resumes the run itself when an approval made through
serve decides the run's last pending gate. An approval made with a local
swamp workflow approve (no --server) never triggers it.
Whether a run is resumed automatically depends on the workflow's top-level
autoResume field and whether it declares inputs:
Workflow autoResume |
Workflow declares inputs |
Resumed automatically |
|---|---|---|
| unset | No | When --auto-resume is on |
| unset | Yes | Never |
true |
Either | Yes, regardless of the flag |
false |
Either | Never |
An automatic resume supplies no new inputs.
When serve resumes the run, swamp workflow approve --server prints
Serve is resuming run <id> automatically and its JSON output carries
autoResumed: true. A resume that is refused or fails is logged, recorded as a
workflow.auto_resume_failed audit event, and leaves the run awaiting a manual
resume. A started resume is recorded as workflow.auto_resume.
| Setting | Default |
|---|---|
Flag: --auto-resume |
off |
Config file: auto-resume |
false |
Environment: SWAMP_AUTO_RESUME |
unset |
Datastore polling
With a remote datastore, three pollers pull changes that peer instances pushed:
config (ConfigPoller), access data such as grants and groups
(AccessDataPoller), and runtime data (RuntimeDataPoller).
--datastore-poll-interval sets all three, and how often serve checks the
managed config extension
lockfile for changes. On a v2 filesystem datastore it sets how often serve
checks the shared grant and group data for changes another process made; see
Multiple instances. It has
no effect on a v3 datastore. Serve logs a warning when the flag is set on any
filesystem datastore without managed config, even though the v2 check uses it.
| Setting | Default |
|---|---|
Flag: --datastore-poll-interval |
30s |
Config file: datastore-poll-interval |
30s |
Environment: SWAMP_DATASTORE_POLL_INTERVAL |
unset |
Token garbage collection
Serve deletes revoked and expired server tokens in every auth mode. The first
sweep runs just after boot, then one runs every --token-gc-interval.
- A revoked token is deleted at the next sweep.
- An expired token is deleted once
--token-gc-grace-periodhas passed since its expiry. - A collected token is removed entirely: its secret, its OAuth access token, its
definition and its data. It no longer appears in
swamp access token list. - Each serve replica sweeps on its own. With a remote datastore, deletions are pushed to the datastore.
- A sweep collects at most 100 tokens, counting tokens it collected and tokens it failed to collect. A larger backlog drains over later sweeps — for example, the first sweep after an upgrade on a long-running deployment, which finds every revoked or expired token ever minted. Tokens that failed in the previous sweep are tried after the others.
- Revoked and expired tokens are refused at authentication whether or not they
have been collected. A backlog only keeps them in
swamp access token listlonger.
| Setting | Default |
|---|---|
Flag: --token-gc-interval |
1h |
Flag: --token-gc-grace-period |
1h |
Config file: token-gc-interval |
1h |
Config file: token-gc-grace-period |
1h |
Environment: SWAMP_TOKEN_GC_INTERVAL |
unset |
Environment: SWAMP_TOKEN_GC_GRACE_PERIOD |
unset |
In serve.yaml, quote these values: token-gc-interval: "0", not
token-gc-interval: 0. See Supported keys.
Config file
swamp serve can read configuration from a YAML file instead of (or in addition
to) CLI flags. By default, the server looks for .swamp/serve.yaml in the
repository directory. Use --config <path> to specify a different location.
Resolution priority
When the same setting is provided through multiple sources, the highest-priority source wins:
- CLI flag (explicit)
- Environment variable
- Config file
- Built-in default
See Configuration Layers for background.
Loading behavior
| Scenario | Behavior |
|---|---|
Default path (.swamp/serve.yaml) exists |
Loaded automatically |
| Default path does not exist | Silently skipped — the server starts with defaults |
Explicit --config <path> exists |
Loaded |
Explicit --config <path> does not exist |
Hard error: Serve config file not found: <path> |
| Invalid YAML | Hard error at startup |
| Unknown keys in the file | Warning logged per key (Unknown key "<key>") — server continues |
Supported keys
The config file uses kebab-case keys matching the CLI flag names. Most flags are
available in the config file, including authentication and TLS settings via the
auth: and tls: nested blocks.
Top-level keys
| YAML key | Type | CLI equivalent |
|---|---|---|
port |
integer | --port |
host |
string | --host |
auth |
object | See auth: block |
tls |
object | See tls: block |
webhooks |
object[] | --webhook (repeated) |
triggers |
object | See triggers: block |
remote-only |
boolean | --remote-only |
schedule |
boolean | --no-schedule (inverted: schedule: false = --no-schedule) |
grant-reload |
string | --grant-reload |
grants-file |
string | --grants-file; a relative path resolves against the repository directory |
grants-dir |
string | --grants-dir; a relative path resolves against the repository directory |
trust-proxy |
boolean | --trust-proxy |
hot-reload |
boolean | --hot-reload |
detach-runs |
boolean | --detach-runs (deprecated) |
verify-on-enroll |
boolean | --verify-on-enroll |
dispatch-env-allow |
string[] | --dispatch-env-allow; [] ships no variables |
ws-idle-timeout |
string | --ws-idle-timeout |
queue-timeout |
string | --queue-timeout |
trusted-hosts |
string[] | --trusted-hosts |
advertise-address |
string | --advertise-address |
heartbeat-interval |
string | --heartbeat-interval |
stale-ttl |
string | --stale-ttl |
reconciliation-interval |
string | --reconciliation-interval |
max-concurrent-runs |
integer | --max-concurrent-runs |
max-concurrent-scheduled-runs |
integer | --max-concurrent-scheduled-runs |
max-runs-per-principal |
integer | --max-runs-per-principal |
max-run-duration |
string | --max-run-duration |
hydration-timeout |
string | --hydration-timeout |
datastore-poll-interval |
string | --datastore-poll-interval |
token-gc-interval |
string | --token-gc-interval |
token-gc-grace-period |
string | --token-gc-grace-period |
dashboard |
boolean | --dashboard |
auto-resume |
boolean | --auto-resume |
enable-internal-api |
boolean | --enable-internal-api |
token-secrets |
object | None. See token-secrets: block |
Duration strings accept the same formats as the corresponding CLI flag: plain
seconds (30), explicit units (2m, 5m), or 0 to disable.
Quote a duration that is only digits. YAML reads an unquoted 0 or 3600 as a
number, and serve refuses to start:
token-gc-interval: "0"
token-gc-grace-period: "3600"Error: Invalid token-gc-interval in /path/to/repo/.swamp/serve.yaml: expected string, got number; quote the value, e.g. "0"List-typed keys (trusted-hosts) use YAML sequences instead of comma-separated
values:
trusted-hosts:
- host.docker.internal
- host.minikube.internalauth: block
Authentication settings are nested under auth:. In the config file,
list-valued keys use YAML sequences; on the CLI the same flags accept
comma-separated strings.
| Sub-key | Type | CLI equivalent |
|---|---|---|
mode |
string | --auth-mode |
admins |
string[] | --admins |
allowed-collectives |
string[] | --allowed-collectives |
allowed-users |
string[] | --allowed-users |
oauth-provider |
string | --oauth-provider |
oauth-client-id |
string | --oauth-client-id |
oauth-client-name |
string | --oauth-client-name |
keep-oauth-client-in-vault |
boolean | --keep-oauth-client-in-vault |
groups-field |
string | --groups-field |
restricted-model-types |
string[] | --restricted-model-types |
restricted-commands |
string[] | --restricted-commands |
group-refresh-interval |
string | --group-refresh-interval |
approve-requires-explicit-grant |
boolean | --approve-requires-explicit-grant |
No environment variable exists for
auth.mode,auth.admins,auth.allowed-collectives,auth.allowed-users, orauth.oauth-provider. These must be set via the config file or CLI flags.
auth.oauth-client-id takes its value from --oauth-client-id, then
SWAMP_OAUTH_CLIENT_ID, then the config file.
swamp serve check-config accepts --oauth-client-id and
--oauth-client-secret-file, as passed to swamp serve. It reports a secret
file it cannot read, a secret with no client ID, and both secret environment
variables set together, as swamp serve would refuse to start. It does not
report a client ID with no secret.
auth:
mode: token
admins:
- "user:abc-123"
- "user:def-456"
restricted-model-types:
- command/shellChecking restriction entries
Serve and swamp serve check-config check restricted-model-types and
restricted-commands entries, from flags and the config file:
| Entry | swamp serve |
swamp serve check-config |
|---|---|---|
A restricted-model-types entry that names no type (@, ::) |
Refuses to start | Failed check |
A restricted-model-types entry without a leading @ that names no registered type |
Startup warning | Warning, built-in types only |
A restricted-commands entry that is not a server command |
Startup warning | Warning |
check-config does not load extension types, so it checks an entry without a
leading @ against the built-in behavior types only, and warns with the reason
unknown-type when the entry is not one of them. Serve checks the same entry
against every registered type at startup. Write an extension type with a leading
@: neither serve nor check-config checks an entry with a leading @,
because an extension type can be installed after startup.
Restriction warnings:
! restricted-model-types entry "command/shel" is not a built-in model type — check the spelling; if it names an extension type, write it as @command/shel
! restricted-commands entry "extension.instal" is not a server command and restricts nothing — command names match exactlyAn entry that names no type fails the check:
✗ swamp serve would refuse to start:
Invalid restricted-model-types entry "::": it names no model type
...
Result: FAILED (swamp serve would refuse to start)Unknown usernames
In oauth mode, serve resolves each username in admins and allowed-users on
the OAuth provider and records the result in its resolved-admins cache. Startup
looks up only names the cache has no record of.
- A resolved name is pinned to its account. Startup never looks it up again, including when the list is edited. To point a name at a different account, remove it, restart serve, then add it back.
- A username the provider reports as not found is skipped with an ERROR log, and stays skipped until it is removed from the list.
- Each entry must be a plain username, with an optional
user:prefix. An entry that contains a colon after that prefix, such asallowed:bob,group:eng, oridp-group:eng, is skipped with an ERROR and is never looked up. Groups are not supported inadminsorallowed-userswith--auth-mode oauth. - Looking up a name needs a credential that can look up users: a login token, or
a collective API token with the
oauth:managescope (see Deploy with Headless OAuth). Startup needs one only when a name is added. A refused lookup (401 or 403) stops startup with an error that names theoauth:managescope. - Any other lookup error stops startup.
- Serve refuses to start if no admin resolves, or if every entry in
allowed-usersis unknown and noallowed-collectivesare set.
swamp serve check-config reports unknown usernames before a deploy. An admin
who cannot log in after a deploy is often listed under a username that did not
resolve: search the serve log for the ERROR line that names it, correct the
entry, and restart serve.
tls: block
TLS certificate paths are nested under tls:.
| Sub-key | Type | CLI equivalent |
|---|---|---|
cert-file |
string | --cert-file |
key-file |
string | --key-file |
ca-cert |
string | --ca-cert |
tls:
cert-file: /etc/certs/server.crt
key-file: /etc/certs/server.keytoken-secrets: block
Server token secrets are stored in the _token-secrets control-plane vault,
encrypted with an AES-256-GCM key. By default that key is stored in the
datastore beside the secrets it encrypts, so read access to the datastore
decrypts every token. The token-secrets: block keeps the key in a vault
outside the datastore instead.
| Sub-key | Type | Description |
|---|---|---|
vault |
string | Vault that holds the key. Any user vault except _token-secrets. Its storage must be outside the datastore |
key |
string | Name of the secret in that vault |
token-secrets:
vault: prod-secrets
key: swamp-token-secrets-key- The key is 32 bytes, hex- or base64-encoded, generated by the operative with a
CSPRNG, for example
openssl rand -base64 32. Swamp never generates it. A key with the same value in every byte is rejected. - The block has no CLI flag or environment variable. Serve reads it from its config file once at startup; changing it requires a restart.
swamp access token mint,rotateandreveal, andswamp worker token create, read the block from.swamp/serve.yamlwhen run locally. Once serve has moved the control plane to an external key, they refuse to run without it.swamp serve check-configreports whether the key is usable, without printing it.- There is no way back to a key stored in the datastore.
See Keep the token secrets key outside the datastore to enable it on an existing deployment.
triggers: block
Trigger overrides define or override scheduled execution for workflows. Each key
is a workflow name; the value is an object with schedule (a cron expression)
and/or inputs (a mapping of input values).
Trigger overrides can also be managed on a running server via
swamp workflow trigger set and swamp workflow trigger remove with --server
(see Hot Reload).
triggers:
nightly-backup:
schedule: "0 2 * * *"
hourly-sync:
schedule: "0 * * * *"
inputs:
target: production
dry-run: false| Field | Required | Type | Description |
|---|---|---|---|
schedule |
No | string | Cron expression (must specify at least schedule or inputs) |
inputs |
No | object | Input values passed to the workflow on each triggered run |
Flags not available in the config file
The following flags are CLI-only and are not recognized in the YAML config file:
--config— identifies the config file itself--repo-dir— sets the repository directory that the config file resolves relative to--no-schedule— useschedule: falsein the config file instead--oauth-client-secret-file— useSWAMP_OAUTH_CLIENT_SECRET_FILEto set it without a flag
The global flags --no-telemetry and --log-level are also CLI-only.
Webhook format in config files
On the CLI, webhooks use a colon-delimited string:
--webhook '<route>:<workflow>:<secret>[:<scheme>[:<header>[:<prefix>]]]'In a config file, each webhook is a YAML object with named fields:
webhooks:
- route: /hooks/github
workflow: deploy-on-push
secret: "@env=WEBHOOK_SECRET"
scheme: github
- route: /hooks/custom
workflow: process-event
secret: "@file=/run/secrets/webhook"
scheme: generic
header: X-Custom-Signature
prefix: "sha256="| Field | Required | Description |
|---|---|---|
route |
Yes | URL path for the endpoint |
workflow |
Yes | Workflow to trigger on valid requests |
secret |
Yes | Signing secret or indirection (@env=, @file=, @vault=) |
scheme |
No | Provider scheme (default: github) |
header |
No | Custom header name (generic scheme only) |
prefix |
No | Expected value prefix (generic scheme only) |
See Webhooks for provider schemes, signature verification, and secret indirection.
Example config file
# .swamp/serve.yaml
port: 8080
host: "0.0.0.0"
auth:
mode: token
admins:
- "user:abc-123"
tls:
cert-file: /etc/certs/server.crt
key-file: /etc/certs/server.key
grant-reload: auto
trust-proxy: true
hot-reload: true
heartbeat-interval: "30s"
stale-ttl: "90s"
reconciliation-interval: "60s"
datastore-poll-interval: "30s"
trusted-hosts:
- host.docker.internal
webhooks:
- route: /hooks/github
workflow: deploy-on-push
secret: "@env=WEBHOOK_SECRET"
scheme: github
triggers:
nightly-report:
schedule: "0 6 * * *"# All settings are in the config file — start with no extra flags
swamp serveEnvironment variables
| Variable | Description |
|---|---|
SWAMP_API_KEY |
Collective API token for server authentication. With serve:* scope, authenticates the server for instance registration and heartbeats. With oauth:manage scope (in addition to serve:*), enables headless OAuth client registration and admin username resolution — no browser interaction needed on first boot. Read fresh from the environment on each start; key rotation requires only a restart |
SWAMP_API_KEY_FILE |
Path to a file holding the collective API key, in place of SWAMP_API_KEY. Setting both is an error. --club-api-key-file takes precedence over it |
SWAMP_SIGNIN_TOKEN |
Signin token issued with the collective token in SWAMP_API_KEY. Lets Serve start while swamp-club.com is unreachable; checked against swamp-club.com at most once an hour. Shown once when the token is created — see Swamp Account Requirement |
SWAMP_REMOTE_ONLY |
Default for --remote-only (true or 1 to enable) |
SWAMP_GRANTS_FILE |
Default for --grants-file. A relative path resolves against the repository directory |
SWAMP_GRANTS_DIR |
Default for --grants-dir. A relative path resolves against the repository directory |
SWAMP_SERVE_URL |
Default server URL for --server on client commands. Takes precedence over SWAMP_SERVER_URL |
SWAMP_SERVER_URL |
Routing alias for SWAMP_SERVE_URL. Used as fallback when SWAMP_SERVE_URL is not set. Precedence: --server flag > SWAMP_SERVE_URL > SWAMP_SERVER_URL. Also scopes SWAMP_SERVER_TOKEN: the token is sent only to the server this URL names |
SWAMP_SERVE_CERT_FILE |
Default for --cert-file / tls.cert-file |
SWAMP_SERVE_KEY_FILE |
Default for --key-file / tls.key-file |
SWAMP_SERVER_TOKEN |
Server token in <name>.<secret> format (alternative to a stored credential). Requires SWAMP_SERVER_URL: the token is used only when the target server matches SWAMP_SERVER_URL. SWAMP_SERVE_URL does not scope it. With SWAMP_SERVER_URL unset, the token is not sent and the CLI logs SWAMP_SERVER_TOKEN is set but SWAMP_SERVER_URL is not — token not sent. Set SWAMP_SERVER_URL to the target server URL or use --token instead. A --token flag overrides it |
SWAMP_OAUTH_CLIENT_ID |
Default for --oauth-client-id / auth.oauth-client-id. Precedence: flag, environment variable, config file |
SWAMP_OAUTH_CLIENT_SECRET_FILE |
Path to a file holding the OAuth client secret. Default for --oauth-client-secret-file. Mutually exclusive with SWAMP_OAUTH_CLIENT_SECRET |
SWAMP_OAUTH_CLIENT_SECRET |
OAuth client secret, used for that run and never stored. Mutually exclusive with SWAMP_OAUTH_CLIENT_SECRET_FILE. Hidden from env.* in expressions and from method subprocesses; extension code runs in-process and can read it |
SWAMP_OAUTH_CLIENT_NAME |
Default for --oauth-client-name / auth.oauth-client-name. Default: swamp-serve-{repoName}-{hostname} |
SWAMP_KEEP_OAUTH_CLIENT_IN_VAULT |
Default for --keep-oauth-client-in-vault / auth.keep-oauth-client-in-vault (default: off) |
SWAMP_WS_IDLE_TIMEOUT |
Default for --ws-idle-timeout (default: 30s) |
SWAMP_QUEUE_TIMEOUT |
Default for --queue-timeout (default: 10m) |
SWAMP_VERIFY_ON_ENROLL |
Default for --verify-on-enroll (true or 1 to enable) |
SWAMP_DISPATCH_ENV_ALLOW |
Default for --dispatch-env-allow / dispatch-env-allow (comma-separated). Set and empty, it ships no variables |
SWAMP_SERVE_EXTRA_HEADERS |
Extra HTTP headers sent by clients through proxies/tunnels (newline-separated Name: value) |
SWAMP_GROUP_REFRESH_INTERVAL |
Default for --group-refresh-interval (default: 4h) |
SWAMP_TRUSTED_HOSTS |
Default for --trusted-hosts |
SWAMP_SERVE_ADVERTISE_ADDRESS |
Default for --advertise-address / advertise-address |
SWAMP_CA_CERT |
Default for --ca-cert / tls.ca-cert on swamp serve; on client commands, the CA certificate trusted for TLS connections to the server |
SWAMP_HEARTBEAT_INTERVAL |
Default for --heartbeat-interval (default: 30s) |
SWAMP_STALE_TTL |
Default for --stale-ttl (default: 90s) |
SWAMP_RECONCILIATION_INTERVAL |
Default for --reconciliation-interval (default: 60s) |
SWAMP_HYDRATION_TIMEOUT |
Default for --hydration-timeout (default: 60s) |
SWAMP_DATASTORE_POLL_INTERVAL |
Default for --datastore-poll-interval (default: 30s) |
SWAMP_TOKEN_GC_INTERVAL |
Default for --token-gc-interval (default: 1h) |
SWAMP_TOKEN_GC_GRACE_PERIOD |
Default for --token-gc-grace-period (default: 1h) |
SWAMP_MAX_CONCURRENT_RUNS |
Default for --max-concurrent-runs (default: 100) |
SWAMP_MAX_CONCURRENT_SCHEDULED_RUNS |
Default for --max-concurrent-scheduled-runs (default: 1) |
SWAMP_MAX_RUNS_PER_PRINCIPAL |
Default for --max-runs-per-principal (no default — no per-principal limit) |
SWAMP_MAX_RUN_DURATION |
Default for --max-run-duration (no default — no limit) |
SWAMP_DASHBOARD |
Default for --dashboard (true or 1 to enable) |
SWAMP_ENABLE_INTERNAL_API |
Default for --enable-internal-api (true or 1 to enable) |
SWAMP_AUTO_RESUME |
Default for --auto-resume (true or 1 to enable) |
SWAMP_APPROVE_REQUIRES_EXPLICIT_GRANT |
Default for --approve-requires-explicit-grant / auth.approve-requires-explicit-grant (true or 1 to enable) |
Authentication modes
none (default — deprecated)
Warning
Deprecated. --auth-mode none will be removed in a future release. See
Set Up Token Authentication.
No authentication. Connections on loopback are accepted without credentials. Off-loopback binding is refused.
token
Token-based authentication. Users authenticate with a server token minted via
swamp access token mint. Unauthenticated connections receive HTTP 401.
Token authentication failures
A connection without a token is rejected with
401 Unauthorized: token required. A presented token that fails authentication
is rejected with 401 Unauthorized: <reason>, and the server logs the reason
with the token name:
| Reason | Meaning |
|---|---|
expired |
The token has passed its expiry. Token garbage collection deletes it once the grace period has passed, after which it fails as no-definition |
revoked |
The token has been revoked. Token garbage collection deletes it at the next sweep, after which it fails as no-definition |
secret-mismatch |
The presented secret does not match the stored secret |
invalid-principal |
The token's stored principal is not user:<id> or worker:<id> — it was minted for an unsupported principal kind. Fix with swamp access token revoke <name>, then mint it again with a valid --principal. swamp auth server-login does not fix it |
no-definition |
No token with the presented name exists |
invalid-format |
The credential is not in <name>.<secret> format, or exceeds 512 characters |
vault-error |
The token's secret could not be read from its vault |
unknown |
Any other failure |
On a 401 or 403, the CLI reports
Authentication failed — run: swamp auth server-login --server <url>. On a 5xx
it reports
Server error (HTTP <status>) while connecting to <url> — check the server log for the cause.
WebSocket token transports
The server accepts tokens on the WebSocket upgrade request via three transports:
| Transport | Header / parameter | Format |
|---|---|---|
| Authorization header | Authorization: Bearer <name>.<secret> |
Standard HTTP Bearer scheme |
| WebSocket subprotocol | Sec-WebSocket-Protocol: bearer.<name>.<secret> |
Single subprotocol value, dot-delimited |
| Query parameter | ?token=<name>.<secret> |
Appended to the WebSocket URL |
When multiple transports are present on the same request, the server uses the first match in this priority order: Authorization header, Sec-WebSocket-Protocol, query parameter.
The Authorization header does not expose the token in URLs. Query-string tokens
appear in proxy access logs, browser history, and Referer headers. See
Token transports on WebSocket
for background on when each transport applies.
See Connect with a token for connection examples.
oauth
OAuth-based authentication. Users authenticate via an OAuth device flow. The
server validates tokens against the OAuth provider specified by
--oauth-provider. See
Set Up OAuth Authentication for
the setup guide.
Credential storage
swamp auth server-login stores tokens in ~/.config/swamp/servers.json. Each
entry maps a server URL to a token. When a command uses --server, the stored
credential is used automatically. The --server flag can be omitted if
SWAMP_SERVE_URL is set — the command resolves the URL the same way other
remote commands do.
swamp auth server-login --server wss://swamp.example.com \
--token 'paul-token.<secret>'
# Or, if SWAMP_SERVE_URL is already exported:
swamp auth server-login --token 'paul-token.<secret>'Webhook endpoints
See Webhooks for the full reference — provider schemes, secret sources, signature verification, and CEL context.
The --webhook flag registers an HTTP endpoint that triggers a workflow on
incoming requests:
--webhook '<route>:<workflow>:<secret>[:<scheme>[:<header>[:<prefix>]]]'Supported schemes: github (default), linear, stripe, slack, jira,
generic. The generic scheme requires a custom header name and accepts an
optional value prefix.
Secret indirection
The <secret> field supports indirection prefixes so signing secrets don't
appear as literal values in process argv:
| Prefix | Source | Example |
|---|---|---|
@env=VAR_NAME |
Environment variable VAR_NAME |
@env=WEBHOOK_SECRET |
@file=/path |
File contents (trailing newline trimmed) | @file=/run/secrets/webhook |
@vault=<name>:<key> |
Named vault key | @vault=prod-secrets:webhook-key |
| (plain string) | Literal value (backward compatible) | mysecret |
swamp serve --webhook '/hooks/github:my-workflow:@env=WEBHOOK_SECRET'
swamp serve --webhook '/hooks/linear:wf:@file=/run/secrets/webhook:linear'
swamp serve --webhook '/hooks/github:deploy:@vault=prod-secrets:webhook-key'The server reads the resolved secret at startup. If the environment variable is unset or the file cannot be read, the server refuses to start.
Related
- Use a Config File — replace long CLI flag chains with a YAML config file
- Configuration Layers — why the four resolution sources have the precedence they do
- Hot Reload —
swamp serve reloadsubcommand, extension bundle reload, and trust list refresh - Webhooks — provider schemes, secret sources, signature verification, and CEL context
- Set Up TLS for swamp serve — certificate setup and reverse proxy configuration
- TLS and Proxies — client-side trust stores and server-side TLS details
- Access Commands — CLI reference for tokens, grants, and groups
- Load External Grants at Startup — load grants from a file outside the repository