IO best practices
Design rules that keep IO journeys secure, fast and easy to change: one flow type per journey, every output connected, secrets in vaults, cached tokens, clean variables, useful logs and safe releases.
Design the journey before you build it
- One purpose per flow. Write it in the flow's description, with what the flow reads, sets and returns.
- One flow type per journey. The type is the journey's address and has one default flow. See Flows and nodes.
- Sign-in journeys start and end at OneWelcome Access. Use Flow - Start (Access) and Flow - End Success (Access) for any journey that signs a user in to an application. Use the browser start node for pages people open directly, such as enrollment or profile pages.
- Reuse before you build. Check your library flows before adding steps: theme, settings, token, find or create a user, send an email. Copies of the same steps drift apart.
- Pick the simplest node that does the job: a Thales service node first, then a standard node, then a script.
- Keep it flat. One or two levels of library nesting. A flow with more than about 20 nodes, or more than five nested calls, usually hides plumbing you can simplify.
- Make the canvas readable. Lay nodes out left to right in the order they run, put failure paths on their own row, and group related nodes in titled comment frames. Don't run Auto-reorder Nodes on a flow someone arranged by hand.
- Give nodes titles that say what they do (
Find user by email, notapi_be 3) and variables names that say what they hold (idsUser,otpForm).
Never strand the user
- Connect every output of every node, including
Failure, every outcome of a nested flow, and ports you think can't fire. An output that fires and goes nowhere ends the run with an error. - Plan for reloads. When a user reloads a page during a journey, IO resumes the session and the page takes its
Failureoutput. Connect each page'sFailureto a step that shows the page again, and run the theme step again first, or the page renders unstyled. - Build one styled error page with a plain message and a way out (start again, or back to the application). Send library failures and HTTP request failures on page paths there.
- Don't show raw error text.
{{flow.error.message}}holds internal details. Log it; show the user a generic message and a reference support can use, such as the time. - Sign-in started by Access can't be restarted inside IO. The
NoTokenandInvalidTokenpages tell the user to go back to the application. Don't add a "try again" button there. - Keep the page on screen while backend steps run: turn on Keep current form on pages whose buttons start API calls, and show your own busy state.
Handle variables deliberately
- Clear before you set again. Setting a variable merges into its old value. Before a retry, a refresh, a loop back to a form, or a second lookup into the same name, clear the variable with Delete Object. Otherwise an old error code or banner survives.
- One frontend setting per variable name. If one node stores
xwith frontend availability on and another with it off, the browser and the server see different values. - Frontend availability off by default. Turn it on only for data a page shows.
- Don't reuse built-in names (
flow,user,request) for your own variables. - Guard unset values. In scripts, check for the literal
{{…}},undefinedandnullbefore using a template value. - Fetch lists at run time. Load catalogs and translations from an API instead of typing long lists into a script.
Security checklist
| Check | How |
|---|---|
| No literal secret anywhere | Secrets, hosts and client IDs come from vaults, in HTTP headers and bodies. See Vaults. |
| Nothing secret reaches the browser | Passwords, tokens and vault values only in variables with frontend availability off. Forms with a password field: frontend availability off. |
| Credential calls only on the server | Token requests and authenticated API calls in Backend - HTTP Request or a Thales service node, never in a page script or Frontend - HTTP Request. |
| No user enumeration | Unknown user and wrong password show the same message. Password reset shows the same "check your email" page whether or not the account exists. Registration doesn't confirm that an account exists. |
| Retries are capped | Limit password and code attempts, for example with a Loop over [1,2,3]: HasNext back to the form, Done to the failure page. |
| Tokens are verified | Check inbound tokens with JWT - Verifier (key and issuer pinned). Never make a decision on a token you only decoded. |
| Values from the URL are checked | Query parameters are user input. Check IDs that authorize anything against a backend source before acting on them. |
| TLS stays on | Keep Skip certificate validation off on calls that carry credentials. Use the CA Certificate field for a private certificate authority. |
| No redirects on credential calls | Keep Follow Redirects off on token, password and other credential calls. |
| Admin actions stay behind operator sign-in | Delete, unlock and role or group changes only in journeys for operators, with groups and roles fixed in the node, never taken from user input. |
| Password changes need proof of identity | Set a password only after a password check or a one-time code in the same flow, for the identity that passed it. |
Keep journeys fast
- Cache service tokens. Store the client-credentials token in a custom session and reuse it until shortly before it expires. If the cache fails, fetch a new token and continue: a cache problem must never fail the journey.
- Never one token per call. At most one token request per library or round of calls. Thales service nodes get and cache their own token, so don't add token steps in front of them.
- Don't wrap every API call in its own library. A chain of token, request, parse and error-handling steps around one call is what a Thales service node replaces.
- Don't log whole objects. Writing a full response body or
{{flow}}to the log can take a second or more, and it leaks tokens and personal data. - Size the session lifetime (the start node's session TTL, in seconds) to the slowest real path, such as waiting for an email code. Sign-in session settings on a login end node are in minutes.
- Let services do their own work. Action Challenge sends email and SMS one-time codes itself, Password Store handles lockout, and deleting an identity removes its password. Don't model these as extra steps.
Log what support needs, nothing more
Use Backend - Log on failure paths and at key events. Its levels are Memory, Trace, Debug, Info, Notice and Warning; there is no Error level.
| Event | Level |
|---|---|
| A step succeeded: user created, sign-in completed | Info |
| An expected outcome the flow handles: user not found, prompt cancelled | Notice |
| A call or step failed | Warning |
| Detail you only need while building | Debug (not kept where the log level is Info) |
[Create user] create FAILED | username={{form.username}} error={{flow.error.message}} response={{idsUser.data}}- Start each line with the flow name and the event, then a few
key=valuepairs someone can search for. - Never log passwords, tokens, vault values, request bodies, whole identities or
{{flow}}. - Frontend - Log writes to the user's browser console: never log anything sensitive there, and remove it before release.
- Connect both outputs of a log node to the same next step, so a logging problem never changes the journey's outcome.
Test, release and change safely
- 1
Test before you activate
Run an inactive flow through a Debug link. Try every path: success, each failure, reloads on every page, the back button.
- 2
Add a version before a big change
Name it after the change. Restore it if the change goes wrong.
- 3
Switch the default last
Make the new flow the default only after testing. Keep the old flow until the new one has proven itself.
- 4
Promote libraries first
When copying to another environment, import library flows first, then point each caller at the libraries' IDs there. The flow types and the nodes the flow uses must exist on the target.
- 5
Check after every import
Open the imported flow and compare its nodes and connections with the source. An import can report success and still lose content.
- 6
Work on one copy at a time
If someone else changed a flow, reload it in the designer before you edit. Saving an old browser tab overwrites their changes.
Watch your journeys after release: how long each run takes, success and failure rates, where users drop out, and the warnings in your logs. Deactivate or delete flows nobody uses any more.