Skip to content

Install and run IdentityFlow

YOUR SERVER Browser OIDC provider Reverse proxy terminates TLS IdentityFlow 127.0.0.1:3000 Workflow project /srv/identity-flow PostgreSQL 17 or 18 signs in HTTPS HTTP discovery, tokens reads stores events
YOUR SERVER Browser OIDC provider Reverse proxy terminates TLS IdentityFlow 127.0.0.1:3000 Workflow project /srv/identity-flow PostgreSQL 17 or 18 signs in HTTPS HTTP discovery, tokens reads stores events
The reverse proxy terminates HTTPS and forwards to the program on 127.0.0.1; the program reads the project at startup and keeps its data in PostgreSQL.

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:

  1. Check which program you have.
  2. Install the project.
  3. Start the program.
  4. Check that the program is ready.
  5. 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/callback as 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:

FileWhat 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:

Terminal window
/opt/identity-flow/identity-flow-v0.3.1-linux-x64 --version

It 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.

Terminal window
cd /srv/identity-flow
npm ci

The 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.

Terminal window
cd /srv/identity-flow
NODE_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-x64
  • NODE_ENV=production selects production mode, which is also what the program uses when NODE_ENV is not set.
  • NODE_CONFIG_FILE names 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 $NAME inside 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:

VariableDefaultPurpose
HOST0.0.0.0Address to listen on. Use 127.0.0.1 when a reverse proxy on the same machine terminates HTTPS.
PORT3000Port to listen on.
ORIGINnoneThe public address browsers use, such as https://flow.example.com. Set it to the origin of auth.oidc.redirectUri.
PROTOCOL_HEADER, HOST_HEADERnone, hostHeaders to read the public protocol and host from, as an alternative to ORIGIN, for example x-forwarded-proto.
ADDRESS_HEADER, XFF_DEPTHnone, 1Header to read the client address from, and how many proxies to skip in X-Forwarded-For.
BODY_SIZE_LIMIT512KLargest accepted request body.
SHUTDOWN_TIMEOUT30Seconds 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
  1. address browsers use
  2. 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.

STARTUP STEPS Configuration read and check authorization.ts compile if set PostgreSQL connect, migrate Hooks, workflows load, deploy Activate access if activate set OIDC discovery fetch document Serve HTTP ready Start stops reason in log then then then then then then first failure
STARTUP STEPS Configuration read and check authorization.ts compile if set PostgreSQL connect, migrate Hooks, workflows load, deploy Activate access if activate set OIDC discovery fetch document Serve HTTP ready Start stops reason in log then then then then then then first failure
The program runs its startup steps in this order and stops at the first one that fails, with the reason in the log; it serves HTTP only after all of them succeeded.

Ask the readiness endpoint /-/ready. It needs no sign-in:

Terminal window
curl --silent --write-out ' %{http_code}\n' http://127.0.0.1:3000/-/ready

It 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:

  1. Workflow engine started ...
  2. 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 changes on the start that activates it;
    • Access Configuration Candidate <digest> is already active on later starts;
    • Access Configuration Candidate <digest> compiled and left inert; set authorization.activate to make it the active Access Configuration when the configuration has no authorization.activate.
  3. 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 × failureThreshold for startup, here five minutes. Startup includes pending database migrations, so raise failureThreshold if 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 tcpSocket probe does not. Because /-/ready does not contact PostgreSQL, a database outage does not make Kubernetes restart the container.
  • Set the pod’s terminationGracePeriodSeconds to cover the whole stop: up to SHUTDOWN_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_TIMEOUT limits only the first part. Measure a stop from SIGTERM to the log line Workflow 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:

Terminal window
export OTEL_METRICS_EXPORTER=prometheus

The 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:

Terminal window
curl --silent http://127.0.0.1:9464/metrics | grep nodejs_eventloop_utilization

It 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: 9464

A 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/v1
kind: PodMonitor
metadata:
name: identity-flow
labels:
# The Helm release name of your kube-prometheus-stack.
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app: identity-flow
podMetricsEndpoints:
- port: metrics
path: /metrics

A 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/v1
kind: NetworkPolicy
metadata:
name: identity-flow
spec:
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: 9464
  • auth.oidc.issuer and auth.oidc.redirectUri must be HTTPS. Plain HTTP is accepted only in development or workshop mode, and only for 127.0.0.1 or [::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; after sessionTtl the person signs in again.
  • IdentityFlow sets identity_flow_oidc_<id> cookies during sign-in, one per sign-in in progress, and identity_flow_session afterwards. All are HttpOnly and SameSite=Lax. They are Secure when auth.oidc.redirectUri uses 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 assigns 0.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.

RANGE ^1.0.0 · SAME CHANNEL Running instance current: 1.0.0 1.0.0 current 1.0.1 in range 1.1.0 newest in range 2.0.0 new major continues on
RANGE ^1.0.0 · SAME CHANNEL Running instance current: 1.0.0 1.0.0 current 1.0.1 in range 1.1.0 newest in range 2.0.0 new major continues on
Each time an instance continues, it moves to the newest deployed version in the range ^ of its current version and the same release channel, never to a new major.

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.

  1. Back up the database.
  2. Stop the 0.2.0 program.
  3. Update the npm packages in your project to 0.3.1 and run npm ci.
  4. 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.
  5. Check the configuration against the configuration reference. If no Access Configuration is active in the database yet, add an authorization block with activate, or nobody reaches the workflow catalog; see Access control.
  6. 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.