Digital Experience

Designing secure contract workflows without building a new app

A controlled, private, expiring document workflow is often a set of decisions rather than a new platform. The CMS you already run can usually carry it.

Digital Experience & CROWorkflow AutomationSecurity Design

“We need people to sign something” turns into a platform evaluation faster than it needs to. It is worth first writing down what the workflow actually requires.

1. Work out the shape before shopping

A team needs to send a named person a document that is already filled in with their details, confirm it is really them, and record what they agreed to. The instinct is to buy a signing platform or commission an application.

The worked example behind this note is Adoption Application and Contract Journey, one strand of a wider rescue operations engagement.

Sometimes that is right. Regulated signatures, audit trails with legal weight, and high volume all argue for dedicated tooling. But a large share of these workflows are narrower than that, and the requirement is really this:

  • The right person can reach the document, and other people cannot.
  • Their details are already in it, so they are not retyping what the organization already knows.
  • The link stops working at a sensible point.
  • Whoever sends it does not need administrator access to do so.
  • The organization can tell whether it arrived.

Every one of those is a design decision rather than a product feature. If the CMS already holds the records, the routing, the user accounts, and the form engine, the decisions can usually be made inside it.

The link is where most of the security actually lives, and where most homegrown versions go wrong.

Use an opaque random token, not an identifier. A URL containing a sequential ID invites someone to try the next number. A long random token carries no meaning and cannot be guessed at useful odds.

Keep personal data out of the URL entirely. It is tempting to pass a name or an email as a query parameter so the page can greet the right person. Do not. URLs end up in browser history, referrer headers, server logs, analytics, and pasted messages. The token should point at server-side state that holds the details, so the sensitive part never travels in the address bar.

Add a second factor the recipient already knows. A short password derived from something the organization and the recipient both hold means a forwarded link is not enough on its own. It also has to be something a volunteer or an administrator can read out over the phone without a password manager.

Give authentication a sensible lifetime. Too short and a multi-step form re-prompts the signer halfway through, which is how people abandon. A few hours is usually right, and it should survive the form’s own save-and-resume behavior.

Let the sender choose an expiry, and invalidate on completion. Two different controls. Expiry handles “this offer is no longer current.” Single-use invalidation handles “this is already signed,” and it is what stops a completed link being reused or forwarded.

The link is not the secretTreat a private URL as something that will eventually be forwarded, screenshotted, or pasted into a group chat, and design so that this is survivable rather than catastrophic.

3. Permissions and delivery

Store the link records out of sight. They are operational state, not content. They do not need to be browsable, listed, or searchable, and keeping them non-public removes a whole class of accidental exposure.

Apply least privilege to the sender. If issuing a document requires administrator rights, one of two things happens: the work bottlenecks on one person, or everyone becomes an administrator. Neither is good. Scope the capability to the actual task so the people doing the job can do it with the access they already have.

Make delivery failures visible. This is routinely skipped. Mail is sent, the code assumes it worked, and nobody finds out otherwise until the recipient says they never got anything. Capture the mail system’s own failure signal and surface the reason to the sender rather than a generic success message.

Always include a plain-text fallback. A styled HTML button is a nice affordance and an unreliable one. Images get blocked, dark modes recolor text, and some clients strip styling entirely. A visible plain URL underneath costs nothing and rescues the cases where the pretty version fails.

4. When the form loads after the page

Here is the obstacle that catches people out, and it is not specific to any one form plugin.

Many form tools load their fields asynchronously. The page arrives first, the real inputs arrive a moment later. That means the usual approach, where the server reads a query parameter and renders the value into the field, simply does not work. The server renders a placeholder; the fields that eventually exist never saw the value.

The workable pattern is to do it on the client, carefully:

  • Wait for each field to actually exist rather than assuming it does.
  • Populate it once, the first time it appears.
  • Trigger the form framework’s own change event, so its conditional logic reacts as though a person had typed.
  • Never reapply the value on subsequent passes.
  • Never overwrite what the person has since typed.

Those last two are the difference between a helpful prefill and a field that fights the user. A naive implementation that re-runs on a timer will cheerfully erase someone’s correction while they are looking at it.

One related trap is worth naming because it fails silently. Where this kind of script gets placed matters. Content management systems often run their page content through filters that escape characters for safety, and those filters can quietly mangle inline JavaScript into something that no longer parses. The script looks right in the source and does nothing in the browser. Emit it from a proper script hook rather than from inside page content.

5. Automation should initialize, not fight

The prefill rules above generalize into something broader.

Automation earns its place by removing the first step of a task. It loses its place when it keeps asserting control after the person has taken over. A field that fills once and then yields is helpful. A field that keeps restoring its own value is an argument the user always loses.

The same test applies to defaults, to suggested content, and to anything that writes into an interface a person is also using: does this run once at the start, or is it still running while they work? If it is still running, it needs a rule for when to stop.

None of this requires a new platform. It requires deciding who the link is for, how long it lives, what it carries, who can issue it, and what happens when delivery fails. Once those are written down, the implementation is usually smaller than the evaluation would have been.