The live Flow overview lists pending Channel messages with their server-assigned IDs and decoded values. Operators can delete a pending message.
Flow Definition Graphs show resource-read edges for AttributeMap and Channel message snapshots explicitly selected by Python RPC decorators. Channel size metadata does not add a resource-read edge because every RPC receives it.
Dex Web searches flows and displays Dex semantic history, step topology, live flow state, time travel points, and stop controls.
The production Web server is Go. It serves an embedded React SPA and translates
same-origin HTTP/JSON requests under /api/ to Dex FlowService gRPC calls.
The FlowService credential never enters the browser.
GetHistoryEvents returns the same input, output, and context step-event
shape for sync, async, and async-fallback execution. Timeline, Execution graph,
Time Travel and the selected event panel use that common structure.
POST /api/blobs/load batches Dex LoadBlobs calls. The browser recursively
hydrates the selected event and current flow state, dedupes by blob kind and ID,
and caches loaded values across tabs. Missing values are labeled
Value blob unavailable. Terminal async failures without a retained invocation
snapshot explain the short-retry behavior in the selected event without raising a
page-level data warning. Neither state exposes blob IDs or storage paths.
Build the SPA and standalone Go binary from the repository root:
make -C web build
./web/dex-web --flow-service-address 127.0.0.1:8801Open http://127.0.0.1:8802. The binary serves the
embedded SPA, /api/, /healthz, and /readyz. It connects to an existing Dex
FlowService; it does not start a Dex Server. SIGINT and SIGTERM allow ten seconds
for HTTP requests to finish before the process exits. /healthz is liveness;
/readyz is readiness and calls FlowService with the configured credential on
every probe. A FlowService outage must not trigger liveness restarts.
Dex Web has no end-user authentication. Anyone who can reach its HTTP port can read and mutate Flows with the configured FlowService credential. Protect the port with network policies or an authenticating reverse proxy before exposing it beyond a trusted local machine.
Every setting below accepts a flag or an environment variable. Flags override
environment values. Settings are immutable after startup, except for token-file
contents, which are checked for rotation at most once per second. Unlisted
environment variables are ignored, including Kubernetes service-link variables
such as DEX_WEB_PORT and DEX_WEB_SERVICE_HOST.
| Flag | Environment variable | Default |
|---|---|---|
--bind-address |
DEX_WEB_BIND_ADDRESS |
127.0.0.1 |
--port |
DEX_WEB_HTTP_PORT_NUMBER |
8802 |
--flow-service-address |
DEX_WEB_FLOW_SERVICE_ADDRESS |
127.0.0.1:8801 |
--flow-rendering-dir |
DEX_WEB_FLOW_RENDERING_DIRECTORY |
empty |
--start-flow-worker-target-headless |
DEX_WEB_START_FLOW_WORKER_TARGET_HEADLESS |
false; deprecated, no effect |
--start-flow-worker-target-pull |
DEX_WEB_START_FLOW_WORKER_TARGET_PULL |
false; deprecated, no effect |
--tls |
DEX_WEB_TLS |
false |
--tls-ca-file |
DEX_WEB_TLS_CA_FILE |
empty; system trust roots |
--tls-server-name |
DEX_WEB_TLS_SERVER_NAME |
empty; verify the target host |
--flow-service-credential-file |
DEX_WEB_FLOW_SERVICE_CREDENTIAL_FILE |
empty; no credential |
--base-path |
DEX_WEB_BASE_PATH |
/; URL path prefix that serves the UI, assets, and /api/ |
--frame-ancestors |
DEX_WEB_FRAME_ANCESTORS |
empty; no page may frame Dex Web |
With --base-path /dex, Dex Web serves everything under /dex/ and returns 404
outside it; /healthz and /readyz stay at the root. index.html is built with
relative asset URLs, and the server rewrites them and adds a
dex-web-base-path meta tag for each response. Frontend code builds every
same-origin URL with webURL from lib/basePath.ts, and names every
browser-storage key that holds Dex Server data, such as recent searches, with
scopedStorageKey. --frame-ancestors replaces frame-ancestors 'none' and
X-Frame-Options: DENY with the listed CSP sources; the handler entry sets these
headers on every response.
The embedded package serves Dex Web for many Dex Servers from a host Go HTTP
server; see the Developer reference page "Embed Dex Web in a Go server". Each
web/vX.Y.Z release publishes it as the module
github.com/superdurable/dex/web/embedded at tag web/embedded/vX.Y.Z, built by
script/release/package_embedded_web_module.py.
TLS encrypts the FlowService gRPC connection and verifies its certificate. A
custom PEM CA file adds certificates to the system trust roots. CA and server
name overrides require --tls. The HTTP listener is plaintext; an HTTPS reverse
proxy can terminate browser TLS.
The credential file contains a bearer token, with surrounding whitespace
ignored. A missing or empty file fails startup. If it becomes empty or unreadable
later, RPCs fail until the file is restored. Credentials stay in the Go process
and are never sent to the browser. Dex Web does not exchange API keys.
An external credential agent can
supply short-lived tokens through the rotating file. Use --tls when sending a
credential to a remote FlowService to protect the bearer token in transit.
POST, PUT, PATCH, and DELETE requests under /api/ require
Content-Type: application/json; a missing or different media type returns HTTP
415. Cross-origin browser requests return HTTP 403. These requirements apply to
standalone Dex Web and dexcli dev, including
Flow search. Non-browser clients without Origin and Sec-Fetch-Site headers
are accepted when they send JSON. When using curl -d, add
-H 'Content-Type: application/json'.
Dex Web uses Go's net/http.CrossOriginProtection. Browsers sending
Sec-Fetch-Site: same-origin work behind TLS-terminating and Host-rewriting
proxies without trusted-origin configuration. Proxies must preserve browser
fetch metadata. For older clients without fetch metadata, the Origin host must
match the request Host, so configure the proxy to preserve the public Host.
Forwarded headers do not override these checks.
This protection does not prevent DNS rebinding. A configurable Host allowlist is a follow-up; restrict accepted hostnames at the reverse proxy and keep direct access to Dex Web's port limited to trusted clients. Masked FlowService errors retain their original diagnostics in server-side logs while browser responses use a generic message.
./web/dex-web --flow-service-address dex.example.com:443 --tls \
--flow-service-credential-file /var/run/secrets/dex/tokenBuild or run the standalone image:
docker build -f web/Dockerfile -t dex-web .
docker run --rm -p 8802:8802 \
-e DEX_WEB_FLOW_SERVICE_ADDRESS=host.docker.internal:8801 dex-webThe image binds HTTP to 0.0.0.0:8802, runs as UID/GID 65532, and includes the
system CA bundle. Mount definition directories and token files read-only with
permissions that allow that UID to read them. Main publishes
superdurable/dex-web:latest; web/vX.Y.Z releases publish
superdurable/dex-web:vX.Y.Z. Changed-component releases select Web when its
source, shared renderer, or protobuf dependency changes. --version reports the
build version.
The Web bridge and command use sdk-go/gen/dexpb for generated FlowService
types. The CLI uses the same protobuf package so a dexcli process registers
dex.proto once.
Standalone mode leaves local Connector setup disabled; use dexcli dev for
that local development workflow.
dexcli devOpen http://127.0.0.1:8802. No Node.js process runs in this mode.
To load Flow Definition Graph JSON files into the independent Flow Rendering page, provide a directory at startup:
dexcli dev --flow-rendering-dir ./build/flow-definitionsDex recursively scans JSON files on every definition-dependent request. Generate them with dexcli visualize SOURCE --json --out ./build/flow-definitions/name; changes become visible without a
Dex Web restart. Invalid JSON, unsupported schema versions, and source read
failures return a typed 503 and make /readyz fail.
Dex Web is one UI. / opens the Flows page at /v1/flows, and the header's
main navigation holds only Flows. Pages that may still change sit in one
Experimental header menu: Designs (/v1/rendering), Work Queue
(/v1/work-queue), and Connectors (/v1/connectors). Each item appears only
when it applies, and the menu is hidden when none does. Designs applies while
the definitions directory holds at least one JSON file; the header checks the
directory again on every page change, so added or removed files show up after
the next navigation. Work Queue applies when the operator catalog
(/api/v2/catalog) serves at least one Flow type with a valid Version 2
definition. Connectors applies when local Connector setup is on and the
loaded Flows use at least one connection. Opening /v1/rendering directly
without definitions shows an empty state that explains how to load them.
Former /v2 URLs redirect to the page that does the same job now, keeping
their query and hash: a former v2 run page and its Deep Dive go to the run page,
Work Queue and Connectors keep their sub-paths, and anything else goes to the
Flows page.
The run page adds tabs after Streams, each marked Experimental and shown
only when it applies: Design when a loaded definition matches the run's Flow
type, Agent session for a Dex Agents session served by the operator catalog,
and Actions when the run's Version 2 definition declares Actions or Display
fields. ?view=<tab> opens a tab directly, such as ?view=session. Design
draws the Flow Definition Graph only; it reads no history and no stored values.
The design graph draws one card per Step. A Flow with no Steps renders
one card per RPC. Each card lists the Attributes the RPC reads and writes and
the Channels it publishes to. Action RPCs show their Action label and required
permission, and Summary and Display RPCs are marked as views. Summary, Display, edits, and
Actions target the current run without accepting a Run ID. Version 1 and
Version 2 files can coexist. Each definition's Flow type is the one Workers
register, so Go Flows need no GetFlowType or GetStepType override unless
dexcli visualize reports a Flow or Step name diagnostic.
Duplicate valid Version 2 definitions for one Flow type, such as two
package-main Flows named main.Flow, make the definition source invalid, and
the error names both files. Invalid analyzer output remains visible on Designs
but does not appear as an operator catalog Flow type.
The Work Queue status filter sends each status's ExecutionStatus value, such as
TimedOut, and shows its label, such as Timed out. The server rejects
the label in a search query.
Work Queue shows Start Flow when the selected definition contains
a v2.start schema. The dialog asks for a Flow ID, a Worker target mode, and the generated
start input. It recursively renders objects, arrays, string-key maps, enums,
booleans, datetime values, optional fields, and nullable fields. Integer input
stays as decimal text until the request is serialized, so int64 values do not
pass through JavaScript floating-point numbers.
Dex Web starts every Flow on pull Workers and sends no Worker address. It does not check whether a pull Worker is currently registered, because registration can change between the check and dispatch.
POST /api/v2/start accepts flowType, flowId, raw JSON input, and an
optional workerTargetPull: true. It returns HTTP 400 for workerTargetAddress
or workerTargetPull: false, because Dex routes Flow work to pull Workers. It
accepts and ignores bypassWorkerHealthCheck. POST /api/v2/worker-health
still reports whether Dex Web can open a TCP connection to a host:port; the
probe does not invoke a Worker method. Start requires X-Dex-Flow-Definition-Revision, reloads the current
definition snapshot, validates every input field again, and rejects unknown
fields. The Server chooses the start Step type and generates the request ID. It derives
skipWaitFor from the validated graph's Start Step phase: execute starts at
Execute, while wait_for+execute retains WaitFor. Missing, repeated, or unknown
Start phases are invalid definitions; the browser cannot supply Step options.
The legacy --start-flow-worker-target-headless and
--start-flow-worker-target-pull flags and their corresponding
DEX_WEB_START_FLOW_WORKER_TARGET_HEADLESS and
DEX_WEB_START_FLOW_WORKER_TARGET_PULL environment variables are deprecated
no-ops. Dex Web accepts them and prints a startup warning whenever either is
provided, including explicit false values. Start Flow always uses pull Workers;
the dialog has no Worker target controls and the catalog has no routing default.
The catalog reports serverEngine: zd or unspecified when the FlowService
does not answer GetServerInfo.
Work Queue is available at /v1/work-queue. Its Working as control
selects one Action permission and filters on the Server-maintained
DexWorkQueuePermissions Search Attribute. The Worker submits the complete
Action permission mapping only when an invocation writes an Action condition
source. Dex Web submits the same mapping only when SetAttributes edits one of
those sources. The Server overlays the writes on authoritative Attribute state
and atomically adds newly matching permissions to the projection. Once added,
a permission remains searchable for that Flow execution. POST /api/v2/search also accepts several workQueuePermissions; they are matched
with OR, then combined with the Flow type and other filters using AND. This
selector does not grant authorization.
A Flow type that offers a requested permission through an Action without a
condition skips the permission filter, because that Action is offered on every
active run and never enters the projection.
/v1/work-queue?view=agents is the agent mode. It lists Dex Agents sessions
waiting at AwaitToolApproval or AwaitQuestionAnswer across every session
Flow type, grouped by Flow type. Each row reads PendingInterrupts through
/api/flows/state and /api/blobs/load. The side panel reuses the Session
tab's read, cards, and Actions from app/agents/. See the Dex Web reference, section Agent inbox.
The local Connectors page is available at /v1/connectors when Dex Web
runs through loopback-bound dexcli dev with a local Flow Definition source.
/v2/connectors and /v2/connections redirect there and keep their sub-path,
query, and hash. The page uses the Work Queue layout: a resizable
sidebar lists the named connections, and the selected one's setup fills the
main panel. The Local store zone at the bottom of the main panel shows the
store directory, the file names inside it, and the launch command, with
Copy buttons that copy the absolute values. The page follows the Dex Web theme.
The page groups Connector Steps by Connector ID and static connection name,
including Python and TypeScript FDG 2.0 definitions. Their Connector metadata
uses releaseTag and package: {ecosystem, name, version}; exact release tags
resolve the same verified release artifacts as existing Go modulePath and
moduleVersion identities. Mixed-language definitions can share one connection
when they name the same Connector release and connection.
Dex Web
resolves the exact official Connector release or release candidate declared by the graph, verifies
release and Studio UI checksums, and loads UI bundles in opaque-origin sandbox
iframes. Each connection view in GET /api/v2/connector-connections carries
displayName, the release manifest's metadata.displayName, from a local
override or a verified official release. Verified
release metadata is cached in memory for the process, and the list waits at
most five seconds for uncached metadata. Without release metadata the field is
omitted, and the page names the Connector by its ID. The connection name is
shown beside it as Connection name.
Without a Studio bundle, it renders the manifest fields directly. The generic
form renders a connector-owned authorization guide with an external provider
link and ordered setup steps. Fields that require user input appear first. Fields
that can be omitted or use manifest defaults remain visible under Optional
settings. Every non-secret manifest default appears below its field in
parentheses; leaving that input blank uses the displayed default. URLs in field
guidance are clickable.
During Connector development, the repeatable
--connector-release-override connector-id=artifact-directory flag on
dexcli dev can replace release resolution for selected Connector IDs. This
allows Flow graphs produced from go.work modules to use the local release
metadata and Studio UI. Connectors labels these sessions as Local
override. Overrides are accepted only by loopback local Connector setup and
retain the normal metadata, checksum, Host API, and safe-archive validation.
The setup tabs expand vertically below the selected named connection. They put
authorization first and disable every Flow configuration until the connection
is ready. Each Connector Step or Trigger binding that declares
ConfigurationUI gets one tab. Dex Web mounts only the selected tab's
sandboxed unit iframes and marks the tab configured after its scoped record has
been saved. The Connector bundle renders only the small pickers and inputs and
reports bounded content heights through a nonce-bound connector.frame.resize
message. Dex Web validates the message source and resizes each iframe without
reading its opaque-origin DOM. Unit ports are merged at their declared JSON
Pointers, so Flow code controls composition without coupling a Connector's
connection screen to one example. The host protocol for this contract is 0.2.
A release with several spec.auth.methods normally selects one of them. With
spec.auth.selection: multiple, one connection holds a non-empty subset of the
methods, such as several model providers. The form then shows one card per
added method, with its guide, credential fields, and configuration fields, and
a Remove button. An Add menu lists the methods not yet added. The section
title and menu use spec.auth.methodLabel, such as Providers and Add
provider; without it they read Authentication methods. Save needs at
least one method. A method's configuration.fields are non-secret fields that
are validated, and required, only while that method is selected.
The connection write sends authMethodId for single selection or
authMethodIds, in add order, for multiple selection. Dex Web stamps the
selection into the stored credentials as auth_method or auth_methods, a
JSON string array. A request that sets either name in credentials is
rejected with CONNECTOR_REQUEST_INVALID. Configuration is validated against
spec.configuration.fields plus the selected methods' configuration fields.
Credentials are validated against the selected methods' fields, and each
method's required fields must be present. Each write replaces the stored
record, so removing a method drops its credential and configuration fields.
Editing a connection prefills its stored non-secret configuration. Stored
credential values are never returned. The connection view lists their names in
storedCredentialFields, and the form shows each stored field as Stored -
leave blank to keep. A blank stored field is sent in keepCredentialFields.
Dex Web copies each listed field from the stored record only when that record
exists, the field belongs to a selected method, and the request does not also
set it. Any other name is a CONNECTOR_REQUEST_INVALID error. OAuth methods
still authorize again with new client credentials.
A spec.configuration.fields entry with studioUnit: {unit, port} renders
that Studio unit inline once the connection is Ready. Before that, the form
shows a note to save the connection first. A required field without a default
stays a plain input, so the first save can succeed. The frame target is
{kind: "connection", unitId, bindings: [{port, jsonPointer: "/<field>"}], value: {<field>: stored}}. A use.configuration.save from it writes the
connection again with the current methods, the stored configuration merged
with the new value, and every stored credential field of the selected methods
in keepCredentialFields.
The connector.host.ready message also carries three optional fields. theme is
the theme the frame paints, light or dark. Dex Web sends the theme the page
paints. When the Dex Web theme changes, it sends every loaded frame a new ready
message, and the bundle repaints in place. It also sets the iframe element's
color-scheme to the same theme, because a frame whose scheme differs from its
element gets an opaque canvas. themeTokens is a flat map from --studio-* CSS
custom property names to values. Dex Web reads each value from an allowlisted
workspace token in app/workspace/css/tokens.css, resolved from the ramps of the sent theme,
so a Dex Web restyle reaches every bundle without a Connector release. The
:root and :root[data-theme='dark'] defaults in the stylesheet below match
those light and dark ramps, and connectorStudioTheme.test.ts checks that they
stay equal.
Each value must match one grammar, which sdk/react in dex-connectors-library
applies too:
- Colors are
#with 3, 4, 6, or 8 hex digits,rgb(R, G, B), orrgba(R, G, B, A), in comma syntax. Channels are integers from 0 to 255 without leading zeros. Alpha is 0 to 1 with at most four decimals. - Radii are
0or a non-negativepxlength with at most three digits on each side of the decimal point. --studio-focus-ringis one box shadow: an optionalinset, two to fourpxlengths that may be negative, and one color.--studio-durationis up to four digits ofms, or seconds with at most two integer digits and three decimals, such as160msor.16s.- A font stack has 1 to 16 comma-separated families. A family is a quoted name
of letters, digits, spaces, and hyphens, or unquoted words that each start
with a letter, optionally after one hyphen. The CSS-wide keywords,
default, andnoneare accepted only when quoted. - A value is at most 256 characters.
Dex Web drops any other value, and the bundle keeps its built-in value for that property.
stylesheet is the whole Connector Studio stylesheet: layout, spacing,
component rules, and the default token values for light and dark. Its only
source is app/connectors/connectorStudio.css. Dex Web imports that file as
text with Vite's ?raw suffix, removes its comments, and sends the result in
every ready message along with theme and themeTokens. A bundle built with
sdk/react replaces its compiled connectorStudioStyles with this text and
then applies themeTokens on top. A restyle of any Studio rule is therefore a
Dex Web change only. The compiled copy in sdk/react is the fallback for hosts
that omit the field, so a restyle does not need to change it. Only new markup
or new classes need a Connector release.
Bundles render only the studio-* class contract, which sdk/react exports as
connectorStudioClassNames: studio-surface, studio-header, studio-muted,
studio-field, studio-actions, studio-button, studio-button-primary,
studio-notice, studio-notice-info, studio-notice-success,
studio-notice-error, studio-notice-attention, studio-options,
studio-option, studio-option-label, studio-option-id,
studio-option-detail, studio-badges, studio-badge, and studio-checkbox.
The sent stylesheet must style every class in the contract and no other
studio-* class. It must also follow these rules:
- It is printable ASCII and at most 65,536 characters. It never contains
</, so it cannot close the frame's<style>element. A bundle ignores a longer stylesheet or one containing</, and keeps its compiled one. - It has a
.studio-<name>selector for every class in the contract. A bundle also ignores a stylesheet that lacks a selector for any class in theconnectorStudioClassNamesit was built with. Renaming or dropping a class inconnectorStudio.csstherefore sends every released bundle back to its compiled stylesheet, and those bundles miss every later restyle until their connectors are released again. Keep every class, and add new ones only. - It has no at-rules such as
@importor@media, no comments, no backslash escapes, no!important, and nourl()orexpression(). - A selector is a
:rootor:root[data-theme='dark']token block,html,body, or starts with astudio-*class. Element names, attribute selectors, and the:hover,:focus-visible,:focus-within,:disabled,:checked, and:not()pseudo-classes appear only after that class. - Token blocks declare only
color-schemeand the allowlisted--studio-*properties, and every value matches thethemeTokensgrammar.:rootdeclares every allowlisted property. - Every other declaration uses only numbers with
px,fr,%,ms, ors,var()references to allowlisted properties without fallbacks, and the layout keywords the test lists. No other function is allowed, and the only color keyword on that list istransparent, so every other color comes from a token. Named colors such asredandcurrentcolorare rejected.
themeTokens are inline properties on the frame's root element, so they
outrank the stylesheet's token blocks. !important is banned because it
would reverse that. connectorStudioTheme.test.ts checks every rule above.
Vitest replaces CSS imports with empty strings, so vite.config.ts lets
connectorStudio.css?raw through for those tests.
Bundles that ignore the fields keep their built-in theme and stylesheet.
The ready message's connection also carries authMethodIds and
configuration for every frame: Step units, Trigger units, and connection
field units. authMethodIds lists the added methods in add order. A
single-selection connection reports its one method, or an empty list when the
release declares no methods. configuration is the stored non-secret
configuration. The protocol version stays 0.2.0.
A release whose studio.setup.backendCapabilities declares
connection.write renders its whole connection setup in the Connector UI.
Dex Web shows that bundle's {kind: "connection"} frame in place of the
manifest form, grants connection.write, and lists the stored credential
field names, never values, as connection.storedCredentialFields in the ready
message. The bundle's connection.save command carries configuration,
credentials, keepCredentialFields, and an optional authMethodId; Dex Web
sends them in the same connection write as the form, so the server validates
them identically, then reloads and confirms Connection saved above the
frame. Such a bundle may also add credentials to provider.command.execute:
values the user typed but has not saved. The broker overlays them on the stored
credentials for that one call and stores nothing, so a model list loads before
the first save. A session without connection.write that sends credentials
is refused with 403. Dex Web serves a connection.write bundle with
img-src 'self' data: instead of also allowing https: images, so a typed
credential cannot leave through an image request; every bundle already gets
connect-src 'none' and form-action 'none'.
Provider resource reads use release-declared Studio commands. The manifest
pins an HTTPS GET URL, a credential field and scheme, fixed query values,
optional fixed headers, and the only path or query parameters the iframe may
supply. The bearer scheme sends Authorization: Bearer <secret>. The
header scheme sends the raw secret in one named header, such as
x-goog-api-key. fixedHeaders holds non-secret values such as
anthropic-version: 2023-06-01.
Header names must be RFC 7230 tokens. Dex Web compares them
case-insensitively and treats _ as -, because CGI-style servers merge the
two. They cannot be Authorization, Accept, Host, Content-Length,
Transfer-Encoding, Connection, Keep-Alive, TE, Trailer, Upgrade,
Cookie, Set-Cookie, Origin, Referer, Forwarded, Proxy-*,
X-Forwarded-*, X-HTTP-Method, X-HTTP-Method-Override, or
X-Method-Override. A fixed header cannot repeat the credential header or
another fixed header. Its value must be 1-256 printable ASCII characters
without leading or trailing spaces and must not contain the credential.
Dex Web checks each command's declaration before it sends that command's
request. A command with an unknown credential scheme or an invalid header
fails with 502, and nothing is sent. The rest of the release still works:
saving credentials, OAuth, UI sessions, and the release's other commands. A
local release override with an invalid declaration fails when Dex Web starts.
Dex Web's generic broker validates the active session and capability, injects
the credential, sends Accept: application/json, bounds a JSON-object
response, and rejects responses containing credential material in any JSON
string or object key. It removes spaces around the stored credential before
sending it, because HTTP drops them on the wire, and it checks responses for
the value it sent. The credential must be printable ASCII.
Provider pagination, filtering, and response projection stay in the
Connector-owned UI bundle; Dex Web contains no provider-specific adapters.
A failed command reaches the iframe as a connector.command.result with
ok: false. Only the Connector UI shows a failed provider.command.execute.
This includes a provider error, a missing credential, and a Dex Web rejection
such as an expired UI session. A failed use.configuration.save, such as one
with a stale Flow Definition revision, also appears in the page error banner.
An unsupported command is reported only to the Connector UI.
Connections persist in $HOME/.dex/connectors/connections.json by default.
Use --connector-config-dir to select another directory. Restarting Dex Web
reloads the file and cached artifacts. OAuth state, PKCE verifier, client
secret, and UI session nonce remain memory-only and are discarded on restart.
The page displays the exact OAuth Redirect URI before authorization. A release
may derive required non-secret credentials from a declared identity endpoint,
which must use HTTPS unless a setting supplies it (see below); Dex accepts a
derived claim only after any declared boolean verification claim is true, and
does not render a duplicate input.
The Local store zone shows the JSON file inside the store directory and
the DEX_CONNECTOR_CONFIG_FILE=... <your-app-command> launch command, and
Copy copies each absolute value. It never returns credential values. Deleting a local credential does not revoke the
provider grant.
Dex Web exchanges the authorization code at the release's tokenEndpoint. It
accepts any 2xx JSON response without an error field. The grant must include
every requested scope. A scope string in the response lists the granted
scopes, separated by spaces or commas. Without scope, Dex Web reads a
scopes string array, which HubSpot returns. A response with neither field
grants the requested scopes, as RFC 6749 section 5.1 allows. A non-empty
refresh_token also proves offline_access, and a non-empty id_token proves
openid. The Microsoft identity platform leaves both out of scope. A grant
that lacks a requested scope fails with CONNECTOR_OAUTH_SCOPE_INSUFFICIENT.
Requested userScopes are checked against Slack's authed_user object the
same way. An authed_user without either field must carry an access_token.
Two optional oauth2 fields shape the code exchange request.
tokenRequestEncoding is form, the default, or json. form sends
application/x-www-form-urlencoded. json sends one JSON object of string
parameters, as Atlassian and Zendesk document. tokenEndpointAuthMethod uses
the RFC 7591 names client_secret_post, the default, and
client_secret_basic. client_secret_post sends client_id and
client_secret in the body. client_secret_basic sends them only as HTTP
Basic credentials, as Zoom, Calendly, and Notion document. Dex Web encodes
client_id:client_secret without form-encoding either value first. Any
other value fails OAuth start with CONNECTOR_RELEASE_INVALID, before the
browser leaves for the provider.
scopes may be empty for providers such as Help Scout and Notion that define
no OAuth scopes. Dex Web then omits the scope authorization parameter,
requires no granted scope, and does not list requested scopes in the form.
A release for an authorization server that the connection owner chooses, such
as a company identity provider, cannot fix its URLs. It writes
authorizationEndpoint, tokenEndpoint, scopes, or a
credentialDerivations endpoint as {"fromSetting": "<field name>"}
instead of a constant. tokenEndpointAuthMethod and tokenRequestEncoding
accept the same form. The name must match a required configuration field of
the selected auth method: type url for an endpoint and stringList for
scopes. Dex Web resolves every reference when OAuth starts, from the
configuration in the start request, which the callback saves under the same
field names. The OAuth session keeps the resolved copy, so the authorization
URL, the code exchange, the granted-scope check, and the identity derivation use
one set of values. A field default applies when the request has no value. A
blank token option also takes the field default, and an empty option selects
the documented default. A missing, null, or blank setting, an empty scope list,
an endpoint that is not an absolute URL, or a token option other than
client_secret_post, client_secret_basic, form, or json fails OAuth start
with 400 CONNECTOR_OAUTH_SETTING_INVALID. The message names the setting and
never repeats its value. Dex Web does not read the oidc block or protocol,
so it performs no discovery and ignores references there. It does not refresh
tokens: a Connector that renews its own credential reads the saved settings.
An endpoint that a release writes as a constant must use HTTPS, as before. An
endpoint that a setting supplies may use http or https. It needs a host and
cannot carry user information or a fragment. Every request that Dex Web sends to
such an endpoint, the code exchange and the identity derivation, follows an
address policy. Dex Web resolves the host once, checks every resolved address,
and dials only an address it checked. It ignores proxies and follows no
redirect for these requests.
- Plain
httpconnects only when every resolved address is loopback or private. - Link-local addresses (169.254.0.0/16 and fe80::/10, which hold the cloud
metadata service),
fd00:ec2::254, and unspecified and multicast addresses are refused even overhttps. - One refused address refuses the whole host. Over plain
http, a single public address is enough. - Every other address needs
https.
OAuth start resolves the token and identity hosts and refuses before the
browser leaves for the provider. The callback checks again when it dials, so a
host that resolves differently later is still refused. Both fail with 400
CONNECTOR_OAUTH_SETTING_INVALID, name the setting and the rule, and send no
request. Dex Web checks the authorization endpoint only for its shape, because
the browser opens it. The OAuth 2.0 HTTP Connector applies the same rule to its
own requests, and connector_address_policy_test.go repeats the address table
of that connector's test so the two cannot drift apart. Even over a permitted
http address, the authorization code, client secret, and tokens cross the
network unencrypted.
Trigger-binding configuration remains in connections.json. Connector Step
operation configuration is stored in the sibling non-secret
use-configurations.json, keyed by connector, connection, operation, Flow
type, and Step type. Writes accept only JSON Pointer paths declared by the
Flow definition. Applications load both files as a startup snapshot through
the Connector SDK; changing configuration requires an application restart.
Environment variables override YAML:
DEX_WEB_FLOW_RENDERING_DIRECTORY
GET /api/flow-definitions and GET /api/v2/catalog return the active revision
as ETag; the operator catalog also returns definitionRevision. The browser sends
that revision as X-Dex-Flow-Definition-Revision for Search, Display, edits,
and Actions. A stale request receives 409 FLOW_DEFINITION_CHANGED; the UI
reloads the catalog, clears stale operation state, and requires Action
confirmation again.
Use the standalone dex-web binary or superdurable/dex-web image described
above, alongside server-zd. Pass --flow-service-address or DEX_WEB_FLOW_SERVICE_ADDRESS
to select that service.
Web starts even when that upstream is unavailable. /healthz reports
process liveness. /readyz validates the current definition snapshot and a
FlowService HealthCheck, returning definitionRevision, source, and definition
count; /api/* returns an upstream error until FlowService is available.
Readiness uses HealthCheck because a search can exceed the builtin visibility
scan budget.
Run standalone dex-web against server-zd. Supply --flow-service-credential-file when
server-zd authentication requires a static token or service JWT:
dex-web --bind-address 0.0.0.0 --port 8802 \
--flow-service-address dex-server-zd:8801 \
--flow-service-credential-file /etc/dex/web-tokenDex Web reads GetServerInfo and reports the engine as serverEngine in
GET /api/v2/catalog. Start Flow always uses pull Workers. The dialog requires
only the Flow ID and Start input; requests can omit Worker target fields.
On server-zd, Time Travel keeps the run ID and creates a new run attempt. The
Run page reloads the newest history window when the run attempt changes, because
events after the fork point no longer belong to the current lineage. The
header summary row replaces Run chain with a Run attempt selector. Choosing
an earlier attempt adds ?runAttempt=N to the page URL and shows that
attempt's lineage. Live history stops for an earlier attempt, and live state
always belongs to the current attempt. Time Travel from that view searches the
selected attempt. A TimeTravelFork event links to the previous run attempt.
GET /api/flows/history accepts an optional runAttempt; zero or absent
selects the current attempt. Each event and GET /api/flows/summary report
runAttempt. POST /api/flows/time-travel
accepts an optional runAttempt and returns the new runAttempt.
web/scripts/run-server-zd-e2e.sh runs the end-to-end test of this mode. It
starts dex-server-zd dev, standalone dex-web, and the Go SDK pull Worker
from sdk-go/integ. Set DEX_SERVER_ZD_BINARY to a dex-server-zd binary from
this worktree or a server-zd release, and build web/assets/dist first.
Requirements are Node.js 22+ and a local dexcli build.
Terminal one starts the Go API bridge on port 8902:
./cli/dexcli dev --web-port 8902Terminal two starts Vite with hot reload on port 8802:
cd web
npm install
npm run devVite proxies /api/* and /healthz to http://127.0.0.1:8902.
cd web
npm ci
npm run check
npm run build
GOWORK=off go test ./...Vite writes production assets to assets/dist/. assets/embed.go embeds that
directory into the Go module and ultimately into dexcli.
The Work Queue list compiles typed filter controls into search queries, including
the built-in activeStepTypes filter. POST /api/v2/search takes flowTypes
in place of flowType to search several Flow types in one query; such a search
accepts built-in filter fields only. The list loads current-run Summary RPCs
with bounded concurrency, and displays Indexed Attributes before ordered Summary fields. Selecting a run validates the Display
contract, supports inline edits for declared primitive fields, and renders
conditional none/object Actions beside the Flow Definition Graph.
Attribute-sourced Action inputs remain hidden. The UI condition is
presentational; Action RPCs must re-check current state. A graph can omit an
Action's condition, and POST /api/v2/actions returns the RPC's output.
Version 2 definitions use uiSlot for reusable display placement and expose
each Action's requiredPermission from the Go SDK Action registration.
User-sourced string Action inputs marked with capture: "qr-code" retain their
text field and add a Scan QR code button. The scanner is loaded only after a
user opens it, prefers the rear camera, and copies the decoded text into the
field without submitting the Action. Manual input remains available when the
camera is missing, denied, or blocked. Camera access requires HTTPS or localhost;
an embedding host must also allow camera access through its Permissions Policy.
Scanning does not grant an Action permission or bypass its condition check.
Pages live under /v1. The Flows page builds a search query from Filters or takes a hand-written
one in Query, with pagination,
saved queries, configurable columns, Indexed Attributes, and timezone
preferences.
The shared definition renderer package, which the docs site still uses and Dex Web no longer draws with, displays one definition JSON. Its legend independently filters control flow, WaitFor, RPCs, resources, SubFlows, and diagnostics. Control flow, WaitFor, RPCs, Channels, Attributes, SubFlows, and diagnostics start visible. Streams start hidden. Flow timeout handlers are always visible as part of the Flow rather than a legend layer.
The Flow Definition renderer uses a compound layout separate from the runtime Execution graph. The lavender Flow frame contains blue Step frames. Each Step stacks orange WaitFor paths above its Execute decisions, with dispatch diamonds for conditional returns. Channels occupy the upper-left resource rail, Attributes share one box, RPC hexagons cross the Flow's left boundary, and folded SubFlows occupy the right rail. Publishers point to Channels, Channels point to consuming WaitFor paths, Attribute writers point to the Attribute box, and the box points to readers. Channel and Attribute relations are hidden until their resource or a related Step or RPC is selected. Resource and SubFlow relations are dashed. Transitions are solid; Execute-failure recovery is solid magenta. Long labels wrap inside their shapes. Backward transitions and self-loops use outer routing lanes. Select an edge to emphasize it and show its complete condition beside the path; its endpoints and source location remain below the canvas. The Mini Map starts collapsed. Flow timeout handlers use a compact Step-like card with their timeout decision, rather than the RPC hexagon. Viewport controls use visible Zoom In, Zoom Out, and Fit View labels. The collapsed Mini Map uses a visible Show Mini Map button. Each graph fits the complete definition into the viewport when it first loads and after its visible layers change.
Designs and the run page's Design tab draw every schema version, 1.0 and 2.0,
with the same design graph (app/design/FlowDesignGraph.tsx); Designs adds
the diagnostics and child Flow links below it. The design graph uses no
borders: depth comes from surfaces and shadow, and colour marks who acts.
Selecting a Step opens its details from the definition alone: explanation,
phase, who acts, what reaches it, what it waits for, and its paths out.
The design graph lays out with elkjs (pinned, EPL-2.0), loaded only with the
canvas. Each Step is drawn with one line in; every other transition is a ↪ chip
on the card it leaves. A dex:group of two or more Steps is a stage that
Collapsed draws as one card and Expanded opens; ⊕ and ⊖ open and close
one stage in place. Its drawing rules and guardrails are in
app/design/canvas/views/README.md.
The design graph draws nothing until its layout has settled, then shows it framed; it never flies in from a default view. A new drawing traces in from its start: each card swells in as the trace reaches it along the lines, and each line draws out of its card. A stage, Collapsed, Expanded or a new direction moves the cards and the view together in one eased motion. Once the reader pans or zooms, the view stays theirs. Reduced-motion settings turn the motion off.
The graph contract, compound layout, React components, and styles live in the
shared flow-definition-renderer
package. Product docs import the same renderer for checked-in Python example
graphs.
The Run page remembers the last selected Overview, Execution graph, Timeline, or
Streams tab across browser refreshes. It loads only the newest history window of
the selected run: at most 1000 semantic events. It finds the newest event ID with
GET /api/flows/wait?nextInternalEventId=1, which FlowStart already satisfies,
and then reads the one page that ends there. It reads FlowStart with a separate
two-event page, so start details and pull routing stay available. Live appends
new events to the newest window and drops its oldest events. Refresh reloads
only the newest window.
The Events tile in the run header names the latest event and pages through events. Execution graph and Timeline share its page; Overview and Streams keep reading the newest window. The tile reads "Latest #10505" and "Showing #9506–#10505 · Page 1 of 11", then offers Latest page (the newest page), Newer page (one page toward newer events), Older page (one page toward older events), and Oldest page (the oldest retained page). Back sits before them after a link jump. Latest page and Newer page are disabled on the newest page; Older page and Oldest page are disabled on the oldest page. Page size beside the buttons sets how many events a page holds, 100 or 1000; the default is 1000, and each viewer's choice is remembered in browser storage. Changing it reloads the Latest page, and every paging read, link jump, Back, and the live newest window use the chosen size. Older page reads up to a page of events before the page. Oldest page reads FlowStart and up to a page of events from the window start, and stops where Older page would start the next newer page. The page that starts at the oldest retained event also shows FlowStart, so it is the oldest page, and a retained history that fits one page shows on one page. Newer page reads up to a page of events after the page; a page that reaches the newest event is the newest page. Live extends only the newest page. An older page stays in place while Live adds events, and Latest page returns to the live newest page. The tile shows the page count only when event IDs skip nothing but the pruned range: every loaded event reports run attempt 1, and the gap after FlowStart matches the pruned range. Servers without run attempts and time travel lineages skip event IDs, so they show no count. The page number also needs the page to be one that Older page reaches from the current newest page; Live moves the newest page, so an older page can lose its number. The newest page is always page 1. Back restores the page's first and last event.
Timeline shows one page. On screens wider than 760 px it is a horizontal
strip, oldest on the left, with the selected event panel below it at full
width. Only the events in view render. Each label is the event's subject, such
as its Step type, Channel name, RPC name, or Attribute name, slanted at one
angle constant in HorizontalTimeline.tsx. The icon shows the event kind, with
a quiet tint per family: Step methods, external inputs, and Flow lifecycle.
Only waiting, failed, and canceled events get a status color, and a failure
also carries a cross. A Step counts as waiting from its WaitFor return until
its Execute starts on the newest page. A cancel decision from a Step or RPC, or
a Flow close, cancels the wait, and a Pending record shows a method the close
interrupted. Each event's accessible name states its derived status, such as
still waiting or canceled. Only an event's dot and label take clicks, so a long
label never selects its neighbor. The overview
above the strip shows one tick per event and drags or steps the view. Left and
Right move the selection, Page Up and Page Down move by a screen, and wheel
input over the strip scrolls it without scrolling the page. Narrower screens
show the vertical list, newest first.
Event numbers are event IDs, so they stay
stable across pages and pruning. Condition-wait and lineage links are resolved
by event ID. In the vertical list, a link whose far end is on another page ends
in an edge stub. The
stub loads the page that holds the far end, highlights it, and offers Back.
Back remembers up to 20 positions and reloads the page it returns to. A far end
that Dex Web cannot locate is labeled not on this page and has no jump. A jump
to an event pruned since the page loaded refreshes the pruned range instead.
A far end removed by the server-zd history window shows as a muted pruned
stub, and the oldest retained page names the removed event range. The
selected event panel lists the same Related events: Show #N selects an
event on the current page, Go to #N loads an event on another page and
hints whether that page is older or newer, and a pruned event reads Outside
the retention window without an action. web/lib/historyRelations.ts is the
only reader of the optional retained-history boundary and related event IDs.
Without them, Dex Web scans the loaded page, and it infers pruning from a gap
after FlowStart. Overview reads the newest window, and the
Execution graph reads the selected page, so Step executions recorded before it
do not appear there. The Execution graph starts a Step whose source is before
the page from an
Earlier events, not loaded node, never from Flow start.
Timeline and Execution graph show an animated loading indicator while waiting
for history. Empty states appear after loading finishes. Refreshing retains
events already on screen. The loading indicator respects reduced-motion settings.
The run header names the Flow ID and run ID once, in the breadcrumb, with
Flow ID and Run ID copy buttons; the page heading is visually hidden. A
summary row leads with the run status, then Started, Closed (only for a closed
run), Duration, the run attempt, and the Events paging control.
Overview leads with Active steps: each Step's phase, Step type, and what it
waits for. A closed run shows its Outcome instead, with any failure message
and the close result. Attributes, Channels, Queued steps, and
Completed outputs follow; empty ones collapse into one quiet line, and long
values show a Show all control. Start input shows the run's start.
Details holds the Flow type, Task queue, sticky Worker ID, request ID, the
first run ID when it differs from the run ID, and the Flow config. Overview has
no selected event; Execution graph keeps the selected event panel in the
sidebar and Timeline shows it below the strip, where the critical-action Review time travel
entry opens the operation with that Step execution and its WaitFor or Execute
method already selected. A history event time entered in the dialog is read in
the page's time zone preference and sent as RFC 3339 UTC. A Time Travel-created run includes a TimeTravelFork
event whose source run links back to the preserved history.
AttributeMap and ChannelMap instances with decimal-only names appear before
other instances and sort by numeric value in live state and event details.
Timeline connects each Step execution's first method event to the Flow start,
RPC, Step decision, or recovery event that scheduled it.
Selecting the first event reveals that source link; selecting a
WaitFor event also reveals its outgoing WaitFor-to-Execute link.
Execution graph nodes separate WaitFor and Execute with distinct colors, channel names, condition icons, and individual event details.
Selecting a Step execution emphasizes it in green, its previous Step in blue, its next Steps in orange, and their connecting arrows.
Close decisions remain in event details and do not create misleading Execution graph edges.
Large Execution graphs extend down the page at readable size. Fan-out ranks spread
horizontally and scale down to fit when possible, with a minimum one-third zoom.
Persisted Step decisions add planned graph branches. A selector-matched branch without a Step event appears Canceled with no execution ID.
SubFlow conditions appear as linked leaf nodes and compact WaitFor cards. Running
and terminal nodes display and link by their generated Flow ID. Original WaitFor
events retain the configured reuse policy and Condition ID.
Methods interrupted by a forced close remain visible as Pending Timeline events
with their last persisted Scheduled or Started phase.
Timeline and Execution graph share structured event details for flow, step method, RPC,
and channel events. A Raw JSON tab preserves the complete server payload.
Below the horizontal Timeline the panel is a container: at 820 px and wider its
sections sit in two columns, and at 1180 px and wider in up to three, such as
Input, Output, and Context. The Execution graph sidebar stays
one column. A scalar or a flat JSON value of about 80 characters or fewer sits on
its label's row. A Step decision reads as one row per target with its input,
named options, and upserted or published names; Decision details expands
the full movements only when they hold more. Attribute rows, read or upserted,
reuse the Overview compact rows and preview each value. Context is one dense
field grid, including pull routing and method options, and hides empty fields.
Blob-backed RPC input and output hydrate through the existing selected-event loader.
The Streams tab accepts a Stream name, reads its retained messages from the beginning,
and then continuously long-polls for the next message. Each message displays its creation
time and source. Stream retention is best effort, so older messages can be trimmed.
External attribute writes appear as Attributes updated events, with the
changed values in the event details. A Timeline row shows its event title; the
event type, such as SetAttributes, shows on hover and in Raw JSON. A row names
a Step's durability only when it differs from the run's configured durability,
and its attempt only after a retry. The absolute time shows only when it
changes from the row above; every row keeps its offset from the run start.
Channel published events list each message's decoded value with a short
message ID, grouped by Channel.
Raw JSON shows hydrated values; missing retained data is labeled unavailable.
SYNC step Context shows the immediately preceding retry failure when available,
including its backend-native error, worker details, named and numeric gRPC
status, and an optional Worker-provided stack. Overview Active steps shows
the latest pending failure on its matching active Step and expands that stack
by default.
While a run is active, Set attribute writes one Attribute and Publish
message publishes one Channel message to that run. Each takes a JSON,
string, int64, double, or boolean value. Int64 values travel as decimal text so
large values keep their precision.
The Overview History settings section shows the run's
FlowConfig.history_window_events as Retention window, read as "Last N
events". An unset field reads Server default. An info icon beside the label
explains that older events are deleted. The page never shows or sends
delete_expired_history_objects, which the server ignores.
While the run is active, Edit changes the window through
PUT /api/flows/config/history, which calls UpdateFlowConfig with only the
changed field. UpdateFlowConfig merges the fields it receives, so the run's
other settings stay unchanged. A saved value cannot be cleared back to the
server default, so the editor offers only a positive window.
Closed runs show no live config and offer no editing.
The selected event's Context uses the same failure view with the stack collapsed.
Sustainable Use License 1.0, with legacy portions under their original terms as described in LEGACY_NOTICES.md.
Flow detail shows the Flow type exactly as registered, the Task queue, which is named after the Flow type, and the sticky Worker ID. Active Steps and Step method history show the serving Worker ID, the sticky ID used at dispatch, and the queue source: Sticky queue or Task queue. Failed dispatches show no available Worker, unknown Step or RPC name, or disconnect reasons. Recorded RPC invocations show their serving Worker ID. Missing metadata displays an em dash.