Install and run IdentityFlow
This page takes you from the delivered files to a running IdentityFlow on a
server you operate. At the end, curl on /-/ready prints ready, people
sign in through your OIDC provider, and the program deploys your Workflow
Definitions at every start. Every configuration key is listed in the
configuration reference.
Before you start: PostgreSQL 17 or 18, an OIDC client, HTTPS in front of the program, and your workflow project; What the server needs has the details. Then follow the page in order:
- Check which program you have.
- Install the project.
- Start the program.
- Check that the program is ready.
- Deploy Workflow Definitions by restarting with the updated project.
Stopping, database migrations and the upgrade from 0.2.0 follow at the end.
- PostgreSQL 17 or 18, with an empty database and a role that owns it. The
first start runs
CREATE EXTENSION IF NOT EXISTS citext, so the role must be allowed to create that extension, or an administrator creates it beforehand. - An OIDC provider with a confidential client for IdentityFlow. IdentityFlow
signs people in with the Authorization Code flow and PKCE. Register
https://<your host>/auth/callbackas the redirect URI. - HTTPS in front of the program, because the sign-in configuration only accepts HTTPS addresses outside a single development machine.
- Your workflow project on the server: Workflow Definitions, a hooks file
that registers your provider,
authorization.ts, the configuration file, and the project’s installed dependencies.
Use a separate database and a separate configuration file for each environment, such as staging and production. 0.3.1 has no tooling to promote a release from one environment to another; you install and start the program in each one.
IdentityFlow 0.3.1 is delivered as files, not as a hosted service:
| File | What it is |
|---|---|
identity-flow-v0.3.1-<target> | The IdentityFlow program: one self-contained executable that serves the web application and the GraphQL API and runs workflows. It needs no Node.js installation. |
identity-flow-test-v0.3.1-<target>/ | The Workflow Test distribution: a folder whose six files must stay together. You use it on developer machines and in CI, not on the server. |
identity-flow-workshop-starter-v0.3.1/ | The starter folder in the workshop archive: an example project with workflows, a hooks file, tests and a local sign-in setup. |
<target> is macos-arm64 (macOS on Apple Silicon) or linux-x64. On Windows,
use the Linux files inside WSL2. The Linux program is statically linked. The
Workflow Test distribution brings its own Node.js and needs glibc, so Alpine and
other musl-based systems cannot run it. 0.3.1 has no container image.
For a server you receive identity-flow-v0.3.1.zip. It holds the Linux x64
program, a README with these steps in short, the changelog and
identity-flow.conf.example, the minimal production configuration from the
configuration reference.
identity-flow-workshop-v0.3.1.zip holds the starter and both distributions
for developer machines.
Your workflow project also depends on the npm packages @identity-flow/api,
@identity-flow/sdk, @identity-flow/binding-graphql and
@identity-flow/testing, version 0.3.1.
Check which program you have. --version reads no configuration and opens no
connection:
/opt/identity-flow/identity-flow-v0.3.1-linux-x64 --versionIt prints IdentityFlow 0.3.1 (identity-flow/0.3.1; <commit>; linux-x64).
--version --json prints the same facts as JSON.
Copy the project to the server and install its dependencies from the lockfile. This step needs Node.js and npm on the server; the program itself runs without them.
cd /srv/identity-flownpm ciThe program bundles every Workflow Definition together with the packages it
imports. Install the same way every time: reinstalling with a different package
manager can change a bundle, which stops the start with
WF_DEFINITION_BUNDLE_CHANGED.
Write the configuration file next to the project. A complete minimal example is in the configuration reference.
cd /srv/identity-flowNODE_ENV=production \NODE_CONFIG_FILE=/srv/identity-flow/identity-flow.conf \HOST=127.0.0.1 \PORT=3000 \ORIGIN=https://flow.example.com \IDENTITY_FLOW_DATABASE_PASSWORD="$(cat /run/secrets/identity-flow-database)" \IDENTITY_FLOW_OIDC_CLIENT_SECRET="$(cat /run/secrets/identity-flow-oidc)" \ /opt/identity-flow/identity-flow-v0.3.1-linux-x64NODE_ENV=productionselects production mode, which is also what the program uses whenNODE_ENVis not set.NODE_CONFIG_FILEnames the configuration file. Use an absolute path with its extension; the lookup rules are in the configuration reference.- The two
IDENTITY_FLOW_…variables fill the${…}references in the example configuration. Use whatever names your file references. At startup the program expands$NAMEinside every environment variable, so a secret that contains$arrives changed. Choose secrets without$.
The program runs in the foreground and writes its log to standard output and standard error. Run it under the process supervisor you already use, such as systemd.
All HTTP server variables
The HTTP server is the SvelteKit Node server and reads these variables:
| Variable | Default | Purpose |
|---|---|---|
HOST | 0.0.0.0 | Address to listen on. Use 127.0.0.1 when a reverse proxy on the same machine terminates HTTPS. |
PORT | 3000 | Port to listen on. |
ORIGIN | none | The public address browsers use, such as https://flow.example.com. Set it to the origin of auth.oidc.redirectUri. |
PROTOCOL_HEADER, HOST_HEADER | none, host | Headers to read the public protocol and host from, as an alternative to ORIGIN, for example x-forwarded-proto. |
ADDRESS_HEADER, XFF_DEPTH | none, 1 | Header to read the client address from, and how many proxies to skip in X-Forwarded-For. |
BODY_SIZE_LIMIT | 512K | Largest accepted request body. |
SHUTDOWN_TIMEOUT | 30 | Seconds to wait for open connections when the program stops. |
These variables belong to the server library the program is built on. Behind
a reverse proxy, set ORIGIN so the program builds the same addresses the
browser sees.
ORIGIN=https://flow.example.com- address browsers use
- origin of redirectUri
The program starts in this order and stops at the first failure, with the
reason in the log. It compiles authorization.ts only when the
authorization block is configured, and activates the Access Configuration
only when authorization.activate is set. The PostgreSQL step runs any pending
database migrations.
Ask the readiness endpoint /-/ready. It needs no sign-in:
curl --silent --write-out ' %{http_code}\n' http://127.0.0.1:3000/-/readyIt prints ready 200 once startup is complete. Until then nothing listens on
the port and the connection is refused. From SIGTERM or SIGINT on, new
connections are refused and connections that are still open get
not ready 503. When a fatal error stops the program, it answers
not ready 503 while it shuts down. The endpoint does not contact
PostgreSQL or your OIDC provider, so it reports that this program finished
starting, not that those services are reachable now. Responses carry
Cache-Control: no-store. The program has no other health endpoint;
/health, /healthz, /readyz and /metrics answer 404 on this port.
Metrics come from a separate port; see Collect metrics.
The log shows the same progress, in this order:
Workflow engine started ...- One line about the Access Configuration:
Access Configuration Candidate <digest> activated, or… activated without a reviewed digest; set authorization.activate.digest = "<digest>" to refuse unreviewed changeson the start that activates it;Access Configuration Candidate <digest> is already activeon later starts;Access Configuration Candidate <digest> compiled and left inert; set authorization.activate to make it the active Access Configurationwhen the configuration has noauthorization.activate.
Listening on http://<host>:<port>
A Workflow Definition that is still a draft adds a line starting with
Skipping deployment of draft workflow:.
Point the startup, readiness and liveness probes at /-/ready. Keep HOST
at its default 0.0.0.0 so the kubelet reaches the port on the pod address:
containers: - name: identity-flow image: registry.example.com/identity-flow:latest ports: - containerPort: 3000 startupProbe: httpGet: path: /-/ready port: 3000 periodSeconds: 5 failureThreshold: 60 readinessProbe: httpGet: path: /-/ready port: 3000 periodSeconds: 5 livenessProbe: httpGet: path: /-/ready port: 3000 periodSeconds: 10 failureThreshold: 3- The startup probe allows
periodSeconds × failureThresholdfor startup, here five minutes. Startup includes pending database migrations, so raisefailureThresholdif a migration takes longer. Liveness and readiness probes start only after the startup probe succeeds. - The liveness probe uses the same endpoint. An HTTP probe needs the program
to answer, so it also catches a process that accepts connections but no
longer responds, which a
tcpSocketprobe does not. Because/-/readydoes not contact PostgreSQL, a database outage does not make Kubernetes restart the container. - Set the pod’s
terminationGracePeriodSecondsto cover the whole stop: up toSHUTDOWN_TIMEOUT(30 seconds by default) for open connections, then the time to shut down the GraphQL API, stop running workflow work and close PostgreSQL connections, plus a margin.SHUTDOWN_TIMEOUTlimits only the first part. Measure a stop fromSIGTERMto the log lineWorkflow engine disposed ...and size the grace period from that; otherwise Kubernetes kills the program before it finishes.
Prometheus can scrape metrics from the program on a separate port. Add this variable to the environment the program starts in:
export OTEL_METRICS_EXPORTER=prometheusThe program then serves /metrics on port 9464 of every interface;
OTEL_EXPORTER_PROMETHEUS_ENDPOINT, OTEL_EXPORTER_PROMETHEUS_PORT and
OTEL_EXPORTER_PROMETHEUS_HOST change the path, the port and the address. The public HTTP port keeps answering /metrics with
404. Check the endpoint on the server:
curl --silent http://127.0.0.1:9464/metrics | grep nodejs_eventloop_utilizationIt prints lines that start with nodejs_eventloop_utilization. Which metrics
the program exports, OpenTelemetry export and the other settings are in
Observability.
The metrics endpoint has no authentication. Do not publish port 9464
through a Service, an Ingress or a load balancer, and let only Prometheus reach
it, with a firewall rule or a Kubernetes NetworkPolicy.
This example assumes the Prometheus Operator, for example from
kube-prometheus-stack, with Prometheus in the namespace monitoring, and an
image you build around the program and your project. Set the variable on the
container and name its metrics port metrics:
- name: identity-flow image: registry.example.com/identity-flow:your-tag env: - name: OTEL_METRICS_EXPORTER value: prometheus ports: - name: http containerPort: 3000 - name: metrics containerPort: 9464A PodMonitor makes Prometheus scrape that port on every pod with the label
app: identity-flow, without a Service in between. kube-prometheus-stack’s
Prometheus picks up only PodMonitors whose release label is its Helm release
name, here kube-prometheus-stack; put in your release name. If your
Prometheus selects PodMonitors otherwise, give this one the labels and the
namespace its podMonitorSelector and podMonitorNamespaceSelector expect:
apiVersion: monitoring.coreos.com/v1kind: PodMonitormetadata: name: identity-flow labels: # The Helm release name of your kube-prometheus-stack. release: kube-prometheus-stackspec: selector: matchLabels: app: identity-flow podMetricsEndpoints: - port: metrics path: /metricsA NetworkPolicy keeps the web application open and the metrics port to the
Prometheus pods in the monitoring namespace, which the Prometheus Operator
labels app.kubernetes.io/name: prometheus. It takes effect only if your cluster’s network plugin
enforces NetworkPolicies:
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: identity-flowspec: podSelector: matchLabels: app: identity-flow policyTypes: [Ingress] ingress: # Everyone may reach the web application and the GraphQL API. - ports: - port: 3000 # Only the Prometheus pods in the monitoring namespace may reach the metrics. - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring podSelector: matchLabels: app.kubernetes.io/name: prometheus ports: - port: 9464auth.oidc.issuerandauth.oidc.redirectUrimust be HTTPS. Plain HTTP is accepted only indevelopmentorworkshopmode, and only for127.0.0.1or[::1].- A started sign-in is valid for at most 10 minutes (
authorizationTtl), a session for at most 8 hours (sessionTtl). Sessions are not extended; aftersessionTtlthe person signs in again. - IdentityFlow sets
identity_flow_oidc_<id>cookies during sign-in, one per sign-in in progress, andidentity_flow_sessionafterwards. All areHttpOnlyandSameSite=Lax. They areSecurewhenauth.oidc.redirectUriuses HTTPS. - A browser keeps at most five sign-ins in progress at a time.
- Every sign-in origin must map to a provider in
identity.originProviders; see Connect your identity provider.
The program deploys Workflow Definitions when it starts. To deploy a new or changed definition, update the project on the server and restart the program.
The program searches workflows.path below workflows.cwd (by default, the
directory of the configuration file) for files matching
**/*.{request,approval,flow,workflow,wf}.?(m)[jt]s or
**/workflows/*.?(m)[jt]s. Test files, hooks files, node_modules and the
other excluded paths in the
configuration reference
are skipped.
The program deploys only the default export of each file, so one file holds
one workflow. A file without a valid default export is skipped with the log
line workflow bundle invalid. Each name belongs to one file: when two files
declare the same name, the program logs workflow bundle name conflict, and
which of the two it deploys is not defined.
A Workflow Definition is a draft unless it sets draft: false. The program
skips drafts and logs Skipping deployment of draft workflow:. Set
draft: false when a definition is ready to run.
Each deployed definition is identified by its name and version. A deployed
version is immutable:
- If a file changes but its name and version stay the same, the start stops
with
WF_DEFINITION_BUNDLE_CHANGED:Workflow Definition "<name>" version "<version>" has a different bundle. … Raise the version before starting. - If a definition has no
version, the program assigns0.0.0+<hash>, which changes with every change to the file. Set an explicit version so you can tell which code an instance ran. - Deployed versions stay in the database. 0.3.1 has no way to remove one.
Running instances move to newer compatible versions of their definition, so a minor or patch version must work for them.
How running instances move between versions
Versions follow semantic versioning, and running instances follow it too. Each
time an instance continues, for example after a dialog is answered, it moves
to the newest deployed version that is compatible with its current one (^
in npm terms) in the same release channel. 1.1.0 and 1.0.1 are compatible
with 1.0.0; 2.0.0 is not. Below 1.0.0 the range is narrow: 0.2.0 is
not compatible with 0.1.0.
So a minor or patch version must work for instances that started on an earlier version of the same major: their recorded steps replay against the new code, and their start parameters are validated again against its schema. A change that running instances cannot absorb, such as a stricter parameter schema or a step that changes its kind, needs a new major version. Instances on the old major then keep running the old major.
Change a definition in place: edit its file and raise version. Do not keep
the old version in the project. The old version stays in the database, and its
running instances load it from there each time they continue, so they keep
running after its code is gone from the project.
Deploy a new version of a workflow shows a version change with a
test. The line under an instance’s
title in the web application shows <name>@v<version> · <release channel>,
and the Operations list shows v<version>.
Send SIGTERM or SIGINT. The program stops accepting connections, answers
/-/ready with not ready 503 on the ones still open, waits up to
SHUTDOWN_TIMEOUT seconds for open connections, then shuts down the GraphQL
API, stops running workflow work and closes its PostgreSQL connections. The
last line is Workflow engine disposed ....
The program migrates the database itself on every start. All pending
migrations run in one transaction under an exclusive lock on the migrations
table.
Migrations only go forward. A program refuses a database that a newer program
has migrated, and stops with WF_DATABASE_MIGRATION_MISMATCH:
Database was migrated by a newer or incompatible IdentityFlow program, or its migration history has a gap.
The message names the recorded migration key and the newest key the program
knows.
Start the program version that migrated the database, or restore a backup taken before the upgrade. Back up the database before you start a new program version.
Only for an installation that runs 0.2.0 today. The upgrade changes the database in one direction; back up first.
Upgrade steps and how to go back
The upgrade changes the database in one direction.
- Back up the database.
- Stop the 0.2.0 program.
- Update the npm packages in your project to
0.3.1and runnpm ci. - Be ready for
WF_DEFINITION_BUNDLE_CHANGED. 0.2.0 silently kept the stored bundle when a file changed under an existing name and version; 0.3.0 stops the start instead. New package versions can change the bundle of a definition whose source you did not touch. Raise the version of each definition the message names: a patch version when its behavior is unchanged, because running instances move to it. If the named definition is an older version of a line that already has a newer one, stop exporting it instead: its instances move to the newest version of the line, and its stored bundle stays in the database. - Check the configuration against the
configuration reference. If no Access
Configuration is active in the database yet, add an
authorizationblock withactivate, or nobody reaches the workflow catalog; see Access control. - Start the 0.3.1 program. It migrates the database.
After step 6, the 0.2.0 program cannot use the database; it fails with
42723: function "uuid_generate_v7" already exists with same argument types.
To go back, restore the backup from step 1. The complete list of changes is on
the Release 0.3.1 page.