Server
One binary, six roles: what flexiq-server runs, every variable that configures it, and how the roles are laid out across replicas.
One binary, six roles: what flexiq-server runs, every variable that configures it, and how the roles are laid out across replicas.
flexiq-server is the scheduler, the executor attach listener, the dashboard,
the sidecar-injecting admission webhook, the gRPC door, the push dispatcher and
the trigger listener in one binary, with no language runtime inside it. Task bodies stay in your own
container — executors dial in and run them there, or the scheduler dials out to
them — so the image is small and identical whichever SDK wrote the tasks.
It is additive, not a replacement: Server & wire is the page about whether you want it. This section is about running it once you do.
FLEXIQ_DSN=/var/lib/flexiq/app.db \
FLEXIQ_LISTEN=127.0.0.1:7777 \
FLEXIQ_DASHBOARD=127.0.0.1:8080 \
flexiq-serverOne process runs any combination of six roles. Five are turned on by naming an address to listen on; push dispatch is turned on by naming a URL to dial out to, and opens no listener at all. Nothing else turns a role on, and nothing turns one on implicitly:
| Role | Turned on by | Serves | Needs FLEXIQ_DSN |
|---|---|---|---|
| Executor attach | FLEXIQ_LISTEN | The worker frame protocol over TCP or a Unix socket | Yes |
| Dashboard | FLEXIQ_DASHBOARD | The SPA, its JSON API, /health, /readiness, /metrics | Yes |
| gRPC door | FLEXIQ_GRPC_LISTEN | flexiq.v1, flexiq.admin.v1, flexiq.executor.v1, the JSON facade, /metrics | Yes |
| Admission webhook | FLEXIQ_WEBHOOK_LISTEN | The mutating webhook that injects executor sidecars | No |
| Push dispatch | FLEXIQ_PUSH_TARGET_URL | Nothing — it dials out instead of serving | Yes |
| Triggers | FLEXIQ_TRIGGER_LISTEN | Inbound webhooks and object-store events that become enqueues | Yes |
One pairing is not optional: FLEXIQ_PUSH_TARGET_SETTLE=grpc — which lets a
target answer 202 Accepted and report later — needs the gRPC door, because
that door is the only inbound surface a push target has. Asking for one without
the other is refused at boot.
At least one of the six must be set. A process with no listener and no push target has no work to do, and it says so at boot rather than idling as a healthy-looking pod that serves nothing. A push-only deployment is a supported shape and has no listener at all — Push dispatch is the page for it.
The webhook is the one role that runs without a database. It rewrites pod specs and never reads a job, so it is exempt; every other role fails at boot without a DSN rather than starting and failing each request.
The scheduler is not a seventh role. Under attach it starts on the first
executor attach — over either door — because with nothing attached there is
nowhere to dispatch to, and claiming jobs anyway would fail them retryably once
placement timed out. A server with FLEXIQ_GRPC_LISTEN and no FLEXIQ_LISTEN
still schedules, as soon as something opens an executor stream. Under push it
starts eagerly at boot instead — a push target is a URL, not a connection, so
there is no first attach to wait for and nothing that would ever start it.
Nor are events. FLEXIQ_EVENTS_FILE sends each job transition the roles
observe out as a CloudEvent, to an HTTP endpoint, a Redis stream, a Kafka
topic or a NATS subject, and opens
no port of its own — see Event egress.
Environment only. The binary takes no flags beyond --help and --version, so
one image is configured entirely by the deployment that runs it. An empty value
reads as unset, which is what makes FOO= in a Compose file mean "off".
| Variable | Default | Meaning |
|---|---|---|
FLEXIQ_DSN | — | Storage connection string. Required by every role but the webhook |
FLEXIQ_BACKEND | From the DSN scheme | sqlite, postgres or redis, when the scheme is not enough |
FLEXIQ_NAMESPACE | Unset | The tenant namespace this process schedules. Required by the gRPC and trigger roles |
FLEXIQ_QUEUES | default | Comma-separated queues to consume. Refused beside FLEXIQ_PUSH_TARGETS, where each target names its own |
FLEXIQ_WORKERS | The dispatch path's slot count | Dispatch concurrency. Must be a positive integer. Refused beside FLEXIQ_PUSH_TARGETS, where each target's capacity sizes its own |
FLEXIQ_MAINTENANCE | on | Whether this process runs retention sweeps |
FLEXIQ_PUSH_DISPATCH | on for Redis, off otherwise | Whether the scheduler wakes on enqueue instead of polling. Unrelated to FLEXIQ_PUSH_TARGET_URL |
FLEXIQ_AUTO_MIGRATE | on | Whether opening storage applies pending schema changes |
FLEXIQ_WORKERS defaults to the slots the dispatch path in use reports — the
capacity attached executors advertise, or FLEXIQ_PUSH_TARGET_CAPACITY under
push. That is the number you want unless you have measured otherwise:
max_in_flight derives from it, and a value above the available slots claims
work nothing has room to run.
The scheduler applies the task and queue overrides set through the admin door, the dashboard or the CLI — backoff, rate limits and concurrency caps — read when it starts, as an SDK worker reads them.
On Redis the scheduler subscribes to one pub/sub channel per queue in its
namespace, so an enqueue wakes it at once only when the job's namespace matches
FLEXIQ_NAMESPACE exactly — a job enqueued with an empty-string namespace does
not wake a scheduler whose namespace is unset. Set FLEXIQ_PUSH_DISPATCH=off when the Redis in front of it refuses
SUBSCRIBE — an ACL without @pubsub, or a proxy without pub/sub. Jobs still
dispatch without it, on the scheduler's poll. An empty value keeps the backend
default here, so write off explicitly; unlike FLEXIQ_MAINTENANCE, a value
this variable does not recognise is refused at startup.
Turn FLEXIQ_AUTO_MIGRATE off when the database credential the server holds is
not allowed to run DDL. The schema then has to be applied out of band before the
server starts — see Backends — and a
server that starts against an unmigrated database refuses rather than serving
half a schema.
| Variable | Default | Meaning |
|---|---|---|
FLEXIQ_LISTEN | Unset | host:port, :port (loopback), or unix:/run/flexiq.sock |
FLEXIQ_ATTACH_TOKEN | Unset | Bearer credential an executor presents in its hello. Minimum 16 characters |
FLEXIQ_LISTEN_TLS_CERT / _TLS_KEY | Unset | PEM certificate and key. Together they terminate TLS on this listener |
FLEXIQ_LISTEN_TLS_CLIENT_CA | Unset | PEM CA bundle. Turns on mTLS beside the cert and key |
An attach connection receives jobs, which makes it a code-dispatch path rather than a data one. A non-loopback TCP bind refuses to start without a token — bind loopback, use a Unix socket, or set one. A Unix socket is exempt because the filesystem is already the boundary. The token is read once and then removed from the process environment, so nothing later in the process reads it back and no child process inherits it.
FLEXIQ_LISTEN_TLS_CERT, FLEXIQ_LISTEN_TLS_KEY and
FLEXIQ_LISTEN_TLS_CLIENT_CA are the same three-variable shape as
the gRPC door's TLS variables, _LISTEN_
in place of _GRPC_: cert and key together turn TLS on, the client CA bundle
beside them turns on mTLS, and all three reload on the same 30-second content
poll — see that page for the boot refusals, the reload mechanics and the
handshake limits, which are shared between the two listeners.
With TLS on, an executor dials tls://host:port rather than host:port, and
the boot log prints attach listener on tls://ADDR. That is the bind address;
FLEXIQ_ATTACH takes tls:// with the reachable host name the certificate
was issued for, since that name is what the executor verifies — a
tls://0.0.0.0:7777 copied from the log fails the handshake. Each connection's handshake runs on its own handshake thread
with a 10 s timeout, so a slow or silent peer stalls only itself. mTLS sits
beside FLEXIQ_ATTACH_TOKEN, never instead of it: the certificate says the
connection may exist, and the token — read only once the handshake succeeds —
says which principal it is. Unset, the listener stays exactly as plaintext as
it has always been.
| Variable | Default | Meaning |
|---|---|---|
FLEXIQ_DASHBOARD | Unset | Bind address for the SPA and its API |
FLEXIQ_DASHBOARD_AUTH | off | off or session |
FLEXIQ_DASHBOARD_ADMIN_USER / _PASSWORD | Unset | Bootstrap the first admin. Only read with session |
FLEXIQ_DASHBOARD_ASSETS | The embedded SPA | Serve the SPA from a directory instead |
FLEXIQ_DASHBOARD_METRICS_TOKEN | Unset | Bearer token for /metrics, for a scraper with no session |
FLEXIQ_DASHBOARD_PUBLIC_READINESS | off | Answer /readiness without a credential |
FLEXIQ_DASHBOARD_INSECURE_COOKIES | off | Drop the Secure cookie flag, for plain HTTP in development |
FLEXIQ_ALLOW_INSECURE | off | Permit an unauthenticated dashboard on an off-host bind |
An unauthenticated dashboard exposes every operator action — cancel, replay,
pause, settings — so a bind reachable off-host with FLEXIQ_DASHBOARD_AUTH=off
fails at boot unless FLEXIQ_ALLOW_INSECURE=1 says the network already
restricts access. The admin password is scrubbed from the environment once read,
like the attach token.
Turn FLEXIQ_DASHBOARD_PUBLIC_READINESS on for an orchestrator probe. An
httpGet probe can carry an Authorization header — but HTTPHeader.value is
a plain string, with no secretKeyRef and no $(VAR) expansion, so the only
way to send one is to write the token literally into the Pod template. This
variable is the alternative to that. It publishes one bit — whether storage answers — to something that has already
reached the port. /metrics stays gated either way. The rest of the dashboard's
auth surface, including SSO, is
its own section.
| Variable | Default | Meaning |
|---|---|---|
FLEXIQ_WEBHOOK_LISTEN | Unset | Bind address for the HTTPS admission server |
FLEXIQ_WEBHOOK_TLS_CERT / _TLS_KEY | — | PEM chain and key. Both required when the webhook is enabled |
Kubernetes calls a mutating webhook over HTTPS and no other way, so unlike the dashboard there is no plaintext mode to fall back to: a bind without a readable key pair is refused rather than served in the clear. The pair is re-read when the files change on disk, because cert-manager renews a Secret in place — the pod keeps running and the mounted files are swapped underneath it, and a server that read them once would serve an expired certificate until something restarted it.
| Variable | Default | Meaning |
|---|---|---|
FLEXIQ_GRPC_LISTEN | Unset | host:port, :port (loopback), or unix:/run/flexiq-grpc.sock |
FLEXIQ_GRPC_KEEPALIVE_INTERVAL | 60 | HTTP/2 ping interval, seconds. 0 sends none |
FLEXIQ_GRPC_REQUEST_TIMEOUT | 30 | Per-call deadline, seconds. 0 is unbounded |
FLEXIQ_GRPC_MAX_CONCURRENT_REQUESTS | 256 | Calls in flight per connection. 0 is unlimited |
FLEXIQ_GRPC_EXECUTOR_STREAM_MAX_AGE | 1800 | Executor stream rotation, seconds. 0 never rotates |
FLEXIQ_AUDIT_RETENTION_DAYS | 90 | Days an audit record is kept — gRPC, dashboard and CLI alike. At least 1. The old name FLEXIQ_GRPC_AUDIT_RETENTION_DAYS is still read |
FLEXIQ_AUDIT_READS_RETENTION_DAYS | The write window | Days a read record is kept. At least 1, never longer than FLEXIQ_AUDIT_RETENTION_DAYS |
FLEXIQ_GRPC_AUDIT_READS | off | Record reads too. on/true/yes/1 or off/false/no/0; anything else refuses to start |
FLEXIQ_GRPC_AUDIT_READS_WINDOW | 60 | Seconds a repeated read is folded into its first record. 0 records every read |
FLEXIQ_GRPC_TLS_CERT / _TLS_KEY | Unset | PEM certificate and key. Together they terminate TLS on this door |
FLEXIQ_GRPC_TLS_CLIENT_CA | Unset | PEM CA bundle. Turns on mTLS beside the cert and key |
TLS termination — reload behaviour, mTLS, and the handshake and boot-refusal rules — is covered in full on The gRPC door.
There is no token variable: callers present a
scoped API token stored in the
database. FLEXIQ_GRPC_TOKEN was the shared secret an earlier build accepted
and is now refused at boot rather than ignored, because a leftover value is
a credential the operator believes is in force. The door itself, and what those
four numbers actually bound, are on
The gRPC door.
FLEXIQ_MAINTENANCE=off does not turn offIt turns off retention — the sweeps that delete archived jobs, dead letters, task logs, task metrics and job errors on their configured windows. That is a job for exactly one replica, because every replica would otherwise run the same deletes against the same rows.
It does not turn off dead-worker reaping or in-flight recovery. Those stay on
in every process, deliberately: recovering a job whose worker died is what keeps
it from sitting Running until its full timeout, and a deployment that switched
it off with maintenance would lose that everywhere except on one pod.
FLEXIQ_MAINTENANCE is a per-process variable, so a three-replica Deployment
cannot set it on for one of its three identical pods. Run the maintenance owner
as its own single-replica release and set FLEXIQ_MAINTENANCE=off on the
replicated one — Kubernetes shows the
two Helm releases that express it.
Retention is on by default, and a queue that has set no windows gets the recommended ones and a warning naming each cutoff — history is deleted on a policy nobody chose, so the log says which one.
Postgres, Redis, gRPC and push dispatch are cargo features. The published image has all four compiled in, so the DSN picks the backend at runtime and nothing has to be rebuilt to enable a role:
cargo build -p flexiq-server --features postgres
cargo build -p flexiq-server --features redis
cargo build -p flexiq-server --features grpc
cargo build -p flexiq-server --features http-targetA binary you built yourself without --features grpc rejects
FLEXIQ_GRPC_LISTEN at boot rather than ignoring it, and one without
--features http-target rejects FLEXIQ_PUSH_TARGET_URL the same way. A
deployment that looks configured and serves — or dials — nothing is the worse
failure.
The HTTP event sink is behind events-http in the same way, and the published
image carries it too: a binary built without it refuses, at boot, an events
document that names an http sink.
Releases publish a distroless multiarch image at
ghcr.io/byteveda/flexiq-server for linux/amd64 and linux/arm64. Distroless
means no shell and no package manager, so docker exec and kubectl debug
behave differently than they do on a Debian-based image. The
container image README
documents the tag policy and what is deliberately not published.
# An attach port dispatches code, so a non-loopback bind refuses to start
# without a token.
export ATTACH_TOKEN=$(openssl rand -base64 32)
docker run --rm -p 7777:7777 \
-e FLEXIQ_DSN=postgres://user:pass@host/db \
-e FLEXIQ_LISTEN=0.0.0.0:7777 \
-e FLEXIQ_ATTACH_TOKEN="$ATTACH_TOKEN" \
ghcr.io/byteveda/flexiq-server:2.1.0On Kubernetes, start from the chart rather than from that command — Kubernetes covers installing it, choosing roles, assigning maintenance, probes and secret rotation.