NAV Navbar

Introduction

Zenoo provides a niche platform for building, defining and orchestrating Digital Onboarding (DO) processes. The Zenoo Hub provides the ability to reconfigure the process to alter the orchestration.

Purpose

The Zenoo Platform has been built from the ground up by experienced developers, product managers and UX experts to solve a number of challenges facing businesses that need to onboard customers.

Zenoo is:

Our aim is to arm developers with a toolkit that makes building, managing and optimizing customer interactions less burdensome and more enjoyable, while improving the bottom line for businesses who embrace our approach. We do this by ensuring each customer interaction is unique and optimized to maximise conversions.

Easy onboarding with Zenoo

The Zenoo architecture has been built with an understanding that not all onboarding channels or customers are the same. With this in mind, The Zenoo Hub can initiate an onboarding experience either when a customer takes action (such as clicking on calculator) or through an API (onboard this customer).

As an example, if a customer applies for a loan on a partner website, the process would be as follows:

  1. Customer visits your website and is asked to complete a DO process.
  2. The client initiates a specific DO process by redirecting Customer to Zenoo Hub Client
  3. The Zenoo Hub Client engages the Hub Backend API to manage the DO process orchestration, processing data, performing checks, and other functions.
  4. The website receives the DO process result and responds with a redirect URL.
  5. The customer is redirected back to a Website using the redirect URL

Architectural Overview

At the heart of Zenoo Platform, Hub is a workflow engine. Each DO process is defined by a workflow using a custom DSL (Domain Specific Language). The workflow engine is used for orchestrating the corresponding DO process, i.e. a series of pages (routes), external calls, etc. This approach makes it possible to change a DO process configuration on-the-fly without rebuilding and redeploying the Zenoo Hub.

The Zenoo stack consists of several components:

Hub overview classic

Digital HUB

Workflow engine

Each workflow execution is assigned an execution context and a (UUID). The UUID identifies a Customer throughout the whole workflow execution.

The execution context stores the current state of a workflow execution as a set of context attributes.

Context attributes

The context attributes define a data model for the corresponding workflow definition. The attributes are hierarchical, and are used for sharing data between different workflow commands and making flow control decisions.

Each attribute can be accessed by its key, such as client.address.city

There are two ways that the context attributes can be set and updated:

config.logo << 'http://logo.png'
products << [product1: "Product1", product2: "Product2"]
route('products') { namespace product }
client.basic << route('basic info')
credit << exchange('credit check')

Execution UUID

Execution UUID is a predefined context attribute which can be used to distinguish a user session or as a specific info in a connector request to provide a relation to each execution for an external or internal service.

uuid - context unique identifier

Usage:

exchange('sendinfo') {
   tosend.sessionId << uuid
}

Workflow Execution

Workflow is managed using the following:

Executor life-cycle

A separate executor handles each workflow execution, including:

DSL

Each DO process is defined by a workflow using a custom DSL (Domain Specific Language).

The workflow DSL provides the following commands:

See the subsections below for an explanation of each.

route

A route corresponds to a page or screen, depending on the Hub Client implementation. The purpose is to display route-specific information and gather customer input.

A route payload, submitted via Hub Client, is validated using the validate payload specification and stored as Context attributes using the namespace.

route('name') {
    uri '/uri'
    export data
    function 'fnc1', 'fnc2, ...
    validate { payload specification }
    namespace namespace
    checkpoint()
    terminal(result)
}

Parameters:

See route examples for more details.

exchange

Makes an external (API) call using a connector (specified by a type) with a connector-specific configuration. To handle a connector failures, a timeout, retries and fallback can be specified. A randomized exponential backoff strategy is used for connector retries. An exchange is executed asynchronously when marked with async(). The connector result is validated using a payload specification and stored as a context attribute under the namespace.

exchange('name') {
    connector 'type'
    config configuration
    fallback {
        workflow
    }
    async()
    timeout 30
    retry 3
    retryBackoff 5
    validate { payload specification }
    namespace namespace
}

Parameters:

path

Executes a registered path (sub-workflow) specified by a name.

path 'name'

function

A function makes it possible to query dynamic data, perform complex calculations or make external calls using exhange(). Functions be used within a workflow as well as by a Hub client via Hub Client API. A function scope, either 'global' or 'route', controls the corresponding function availability via the REST API. Global functions are available throughout the whole workflow execution, whereas a 'route' function is only available within a specific route.

function('name') {
    script
}

match

Executes a workflow when an expression is true. The expression can contain context attributes.

match (expression) {
    workflow
}

exist

Executes a workflow when an attribute is set.

exist (attribute) {
    workflow
}

switch / case

The switch statement matches expression with cases and executes the matching case.

It's a fallthrough switch-case. You can share the same code for multiple matches or use the break command.

It uses different kinds of matching like, collection case, regular expression case, closure case and equals case.

switch (expression) {
    case "bar":
        route "Bar"

    case ~/fo*/:
        route "Foo"
        break

    case [4, 5, 6, 'inList']:
        route "Matched
        break

    default:
        route "Default"
}

loop-until

Executes a workflow until an expression is true. Optionally, you can specify a maximum number of the workflow block is executed.

loop {
    workflow
} until (expression)

loop(number) {
    workflow
} until (expression)

lookup

Looks up existing (active or expired) execution context matching a condition. The condition is specified as a context attribute. An execution context is matched if it contains the condition attribute. Active executions are tried first. If no active execution context matches the condition, the expired executions are tried. This applied only when execution are persisted, i.e. the logger profile is active.

lookup(client.mobile)
existing << lookup(pco.applicationId)

Command registration

In a command registration, you register and use a command configuration by employing a name. The purpose is to enable reuse of a command configuration in a workflow script.

The following command configurations can be registered: - route(name) - exhange(name) - path(name) - function(name) - global function(name)

Here's a general example of registration for a route command:

register {
    route('basic') {
        uri '/basic'
    }
}

route 'basic'
route('basic') {
    terminal()
}

In addition, a registered command configuration is useful as a template that may be refined further in a workflow script.

The following example illustrates a command registration, usage (as-is), and configuration refinement.

As seen in this example, a global exchange configuration can be registered to configure a default exchange fallback, timeout, and retry policy.

register {
    exchange {
        retry 5
        timeout 15
        fallback {
            route 'error'
        }
    }
}

Payload validation

The validate block is used for route and exchange results validation.

It specifies a result (fields) structure and data constrains.

A field is defined by a name, data constrains (validators) and nested fields. The default validator for a field is mandatory, i.e. a field is mandatory if listed.

You can use the following validators - string() a field must be a string - number() a field must be a number - truefalse() a field must be a boolean - file() a field must be a file descriptor - file mimeType a field must be a file descriptor and match the mime type, e.g. application/pdf, image, etc. - oneOf value1, value2, ... a list of values, a field data must be one of the values - regex ~/pattern/ a regular expression to match a field data, the expression can be a string or a groovy regex pattern syntax

Here is an example:

validate {
    firstname
    lastname
    address {
            city { values "Prague", "Paris" }
            zip { regex ~/^[0-9]{5}(?:-[0-9]{4})?$/ }
        }
}

See validate examples for more details.

Workflow validation

Connectors

Connectors are the integration points of the entire workflow orchestration. They are wrapped by exchange commands to be used within the DSL.

Throughout the workflow execution, external/internal providers can be called by means of exchanges that trigger the connectors. The connectors fetch results and decide in each step what to do with the provider responses accordingly.

For a typical onboarding journey, connector use cases include the following:

Connector DSL examples

Here are some examples of common connector usage:

Sending one-time 2FA passcode

exchange('Send One-Time Passcode') {
    timeout 5
    connector('send-otp')
}

Authenticating user ID

exchange("Authenticate Document") {
    namespace upload
    timeout 60
    connector('authenticate-document')
}

Getting loan information

exchange('Loan Summary') {
    namespace loan
    connector('fetch-loan-summary')
    config clientKey: personal.clientKey
    validate {
        loanId
        loanAmount
        loanTerm
        totalRepayment
        monthlyRepayment
    }
}

Calling credit bureau

exchange('Check Credit Bureau') {
    connector('credit-check')
    config clientKey: personal.clientKey
    validate {
        creditCheckResult {
            oneOf 'SUCCESS', 'FAILED', 'ERROR'
        }
    }
}

Fetch decision

exchange('Contract Decision') {
    namespace decision
    connector('fetch-contract-decision')
    validate {
        responseFlag {
            oneOf 'ACCEPT', 'DECLINE', 'REFER', 'SOLVENCY_CHECK', 'COUNTER_OFFER'
        }
    }
    timeout 30
}

Send reminder

exchange('Send Reminder') {
    namespace crm
    connector('send-reminder')
    config type: reminderType
}

How to use it in workflow

route('Mobile number')
exchange("send OTP")

route('OTP entry')
exchange("verify OTP")

route('Authenticate Document')
exchange("Authenticate Document")

Metrics

The Zenoo Hub employs Micrometer—a vendor-neutral application metrics facade—to integrate with the most popular monitoring systems.

Micrometer has a built-in support for AppOptics, Azure Monitor, Netflix Atlas, CloudWatch, Datadog, Dynatrace, Elastic, Ganglia, Graphite, Humio, Influx/Telegraf, JMX, KairosDB, New Relic, Prometheus, SignalFx, Google Stackdriver, StatsD, and Wavefront.

The following metrics will automatically register:

Executor metrics

Execution metrics

JVM metrics

In addition, you can register custom metrics in a workflow script using the metrics DSL.

Hub Backend REST API

The Hub Backend provides a REST API for Hub Clients and Backoffice.

Hub Client API

The Hub Client API is used by Hub Clients to start, resume, and get the current state of a workflow execution.

Route resource

Route resource represents a route that will be rendered by a Hub client. It contains these fields:

Security

The execution API endpoints are secured using JWT tokens. A token is generated when a workflow execution starts. This token is then used for accessing the corresponding workflow execution, identified by uuid.

The token expiration is set to 8 hours and can be modified using the jwt.expiration property.

When using the execution API endpoints, the token must be sent in the Authorization header.

Authorization: Bearer {token}

Start new execution

POST /api/executor/{name}

Creates and starts a new executor for a workflow, which is specified by a name. Optionally, omit the name if the Hub instance has only one workflow.

Optionally, the body can contain a JSON payload that can be then accessed in a workflow script using it.

Response

Example:

HTTP/1.1 201 Created
Content-Length: 80
Content-Type: application/json;charset=UTF-8
Location: http://localhost:8080/api/execution/511b0441-d6d0-4a8a-8003-22ef52740847

{
    "uuid": "511b0441-d6d0-4a8a-8003-22ef52740847",
    "uri": "/api/execution/511b0441-d6d0-4a8a-8003-22ef52740847",
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co"
}

Query the current route

GET /api/execution/{uuid}

Query the current route using an executor with uuid. Since it must wait until the current route is ready, the response may take a few moments.

Response

Example:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co

{
  "uuid": "89828e1e-c834-42a2-86f1-893209f63ab5",
  "uri": "/product",
  "terminal": false,
  "backEnabled": false,
  "export": {"product1": "Product1", "product2": "Product2"},
  "payload": {"product": "product1"}
)

Submit data and resume execution

POST /api/execution/{uuid}

Submit data gathered at the current route and resume a workflow execution using an executor with uuid.

Request

Parameters:

Example:

POST /api/executor/c973a8e7-eb24-4e55-980f-f2ea0fff680e HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co

{
    "uuid": "3a0d231f-12b8-47b3-a495-b9418db294b3",
    "payload": {
        "firstname": "Joe",
        "lastname": "Bloke",
        "id_card": {
            "uuid": "a3c6c1ec-ae02-47a6-8cac-8760521f2ed8",
            "fileName": "my_cool_id_card_photo.png",
            "mimeType": "application/png",
            "size": 123123,
            "expiredOn": "2020-01-01T12:00:00Z"
        }
    }
}
Response

Example response:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co

{
  "uuid": "89828e1e-c834-42a2-86f1-893209f63ab5",
  "uri": "/product",
  "terminal": false,
  "backEnabled": false,
  "export": {"product1": "Product1", "product2": "Product2"}
)

This is an example of a route validation error response:
HTTP/1.1 422 UNPROCESSABLE ENTITY
Content-Type: application/json;charset=UTF-8

{
    "errors": [
        {
            "field": "mobile",
            "message": "Required"
        }
    ]
}

Reverse execution to the previous route

POST /api/execution/{uuid}/back

Execution move back to the previous route which has an executor with the uuid.

Parameters:

Request

Form of the request:

POST /api/executor/c973a8e7-eb24-4e55-980f-f2ea0fff680e/back HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co

{
    "uuid": "3a0d231f-12b8-47b3-a495-b9418db294b3",
    "payload": {
        "firstname": "Joe"
    }
}
Response
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8

{
    "uuid": "fc05e808-9521-4268-af2d-dcc68a6eb4c9",
    "uri": "/basic",
    "terminal": false,
    "backEnabled": false,
    "payload": {
        "firstname": "Joe",
        "lastname": "Bloke"
    }
}

Execute function

POST /api/execution/{uuid}/function

Creates and starts a new executor for a function specified by a name with payload as input data.

Parameters:

Request
POST /api/execution/871fe310-c9c9-4f54-ac2d-80380cb235c0/function/ HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co

{
 "name": "exchange"
}
Response

Example response:

HTTP/1.1 201 Created
Content-Type: application/json;charset=UTF-8
Location: http://localhost:8080//api/execution/871fe310-c9c9-4f54-ac2d-80380cb235c0/function/9899999f-0845-4027-9cfd-09d1f109a420

{
    "uuid": "9899999f-0845-4027-9cfd-09d1f109a420",
    "uri": "/api/execution/871fe310-c9c9-4f54-ac2d-80380cb235c0/function/9899999f-0845-4027-9cfd-09d1f109a420"
}

Query function result

GET /api/execution/{uuid}/function/{functionUUID}

Query the result of executed function with functionUUID within execution with uuid. Since it responds when the result is ready, it may take a few moments.

Response

Form of the response:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1MTFiMDQ0MS1kNmQwLTRhOGEtODAwMy0yMmVmNTI3NDA4NDciLCJzdWIiOiJleGVjdXRvciIsImlhdCI6MTU1NzI0NjE4OSwiZXhwIjoxNTU3Mjc0OTg5fQ.2GVAuboArO8k1G48CY1ojFdypO9zm9u2ZubCE7Qa-Co

{
    "hello": "dummy"
}

Hub File Cache REST API

The Hub provides a REST API for caching client-uploaded files. This gives you the ability to use only cached file descriptors for form processing and avoid using multipart data. Also, this feature improves user experience since the user can upload files separately—and it is faster than a batch upload.

POST /api/files/cache

Uploads a new file to cache. The file is a multipart representation of uploaded file.

Request

Here's an example request:

POST /api/files/cache HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data

Response

HTTP/1.1 201 Created
Content-Type: application/json;charset=UTF-8

{
  "uuid": "89828e1e-c834-42a2-86f1-893209f63ab5",
  "fileName": "my_file.pdf",
  "mimeType": "application/pdf",
  "size": 123123,
  "expiredOn": "2020-01-01T12:00:00Z"
)

DELETE /api/files/cache/{uuid}

Removes a file containing a uuid from the cache and deletes it from the server.

Request

Here's an example request:

POST /api/files/cache/9828e1e-c834-42a2-86f1-893209f63ab5 HTTP/1.1
Host: localhost:8080
Content-Type: application/json;charset=UTF-8

Response

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8

HUB Client

Targets

The HUB Client consists of these four components:

HUB client

HUB Client Target is a folder with appropriate source files (YAML, LESS, Assets etc.) and core dependecies: hub-client-target-builder, hub-client-core, and hub-client-common.

YAML

The YAML used in Target Builder is a standard YAML that has been extended with some specific YAML tags.

In the YAML files, there are two "reserved" fields in the root:

Syntax

List of supported tags

Compile-time tags

Run-time tags

Deprecated tags


!include compile-time tag

Syntax

Short version (without properties):

!include ./path/file.ext

Long version (with properties):

!include file: ./path/file.ext
property1: 'some'
property2: 123
Examples
list:
  - !include ./info.md
  - !include file: ./more_info.yml
    title: 'Hello'
    withoutHeader: true
something: !include ./something.yml


!ref compile-time tag

Syntax
!ref components: component_name
Examples
components:
  header: !include file: ./components/header.yml
    title: "Default title"
  bodyItem: !include ./components/body-item.yml
  footer: !include ./components/footer.yml

...

items: - !ref components: header title: 'Welcome' - !ref components: bodyItem name: 'First one' - !ref components: bodyItem name: 'Second one' - !ref components: footer


!property compile-time tag

Syntax

Short version (without default):

!property some_prop

Long version (with default or required):

!property name: some_prop
default: 'Some default value'
required: true
Examples
items:
  - !property prop1
  - !property name: prop2
    default: 'Prop2 is not here :-)'
  - !property name: prop3
    required: true


!condition compile-time tag

Syntax
!condition include: typeof some_prop === "boolean" && some_prop === true
!condition omit: typeof some_prop === "string" && some_prop !== "foo"
Examples
items:
  - name: 'this item is not here :-('
    !condition include: typeof some_falsy_prop === "boolean" && some_falsy_prop === true
  - name: 'this item is here :-)'
    !condition omit: typeof some_falsy_prop === "boolean" && some_falsy_prop === true

!component run time tag

Syntax
!component as: some_component
property: 'foo'
Examples
items:
  - !component as: div
    items:
      - !component as: h1
        items: 'Hello'
      - !component as: HubClientMagicComponent
        doMagic: true

!repeat run time tag

Syntax
!repeat some_item:
  - name: 'Some 1'
  - name: 'Some 2'
  - name: 'Some 3'
component:
  !component as: span
  items: !expression some_item.name
Examples
items:
  - !repeat car: !expression export.cars
    component:
      !component as: span
      items: !expression car.name
  - !repeat pair:
      - name: 'Some 1'
        value: 'Val 1'
      - name: 'Some 2'
        value: 'Val 2'
      - name: 'Some 3'
        value: 'Val 3'
    component:
      !component as: div
      items:
        - !component as: span
          items: !expression pair.name
        - !component as: span
          items: "!expression '(' + pair.value + ')'"

!expression run time tag

Syntax

Short version (without parameters):

!expression 'some_expression'

Long version (with parameters):

!expression eval: 'some_expression'
parameter_one: 'Some parameter value'
Examples
items:
  - !component as: span
    items:
      - !expression 'exports.someExportField'
  - !component as: span
    max:
      !expression eval: 'parseInt(param1) - param2'
      param1: !expression 'exports.someExportField'
      param2: !property test1


!function run time tag

Syntax

Short version (without parameters):

!function 'some_expression'

Long version (with parameters):

!function eval: 'some_expression'
parameter_one: 'Some parameter value'
Examples
items:
  - !component as: div
    onClick: !function 'setSomething(arg1 + "A")'
  - !component as: div
    onClick:
      !function eval: 'doSomething(parseInt(param1) - param2)'
      param1: !expression 'exports.someExportField'
      param2: !property test1
  - !component as: div
    onClick: !function |
      var a = "A";
      var b = "B";
      return a + b + " = done";

!t run time tag

Syntax

Short version (without params):

!t Some text

Long version (with params):

!t text: Some text with {paramOne} and {paramTwo}
paramOne: 'Zenoo'
paramTwo: !expression 'exports.someExportField'
Examples
items:
  - !component as: span
    items:
      - !expression 'exports.someExportField'

!cx run time tag

Syntax
!cx args:
  - 'classOne'
  - 'classTwo'
  - !expression 'exports.someClassName'
Examples
items:
  - !component as: span
    className:
      !cx args:
        - 'classOne'
        - 'classTwo'
        - !expression 'exports.someClassName'

Target Builder

The Target Builder is a tool to process Target files and combine the files with the contents of compiled @zenoo/hub-client-core into releaseable package.

Target Builder uses the index.yml file as entry point, then combines and compiles all files that are included inside this file and also process files in assets folder. These files are supported: YAML, JSON, HTML, MD, LESS, and CSS.

Follow these steps when running Target Builder:

  1. Process assets folders and put it to output folder.
  2. Process the entry point YAML file, and recursively process all includes (for more details, see !include).
  3. Process/compile all other files (such as less, md, etc.).
  4. Combine all output into one large configuration.json file and one styles.css file and place into the output folder.

Assets

While building a target, two folders of assets are being processed:

Target Builder collects all of the content in these two folders, adds a random hash postfix to the filenames (to prevent caching issues), and places it into the /assets output folder. All references to assets will be replaced with new hashed names, both in YML and LESS/CSS files.

In case of collisions between <target>/src/assets and @zenoo/hub-client-common/lib/assets, the file from <target>/src/assets will have a higher priority. Use this prioritization to replace some "default asset" with an asset specific only for this target.

IMPORTANT

The format to reference an asset should be: /assets/some_file.ext.

Other reference formats such as ./../assets/some_file.ext will not be resolved properly.

Styles

In root of each YAML which is included in any depth of target scruture you can define styles field which can contain array of CSS or LESS files to include it into target build.

In finally steps of target build is all styles filtered by unique, that means you can import one style file in multiple components as many times you want, and on output styles.css will be each file only once.

Example:

styles:
  - !include ./style.less
  - !include ./other-style.css

Environment-specific entry points

To build a target with an environment-specific configuration in Target Builder, you can specify a different entrypoint by creating a different index.yml file.

The format of this environment-specific index.yml file must be as follows:

<target>/index.{ENVIRONMENT}.yml

Example: <target>/index.production.yml

Within this file, simply include the main index.yml entry file:

<<<: !include index.yml
serverUrl: 'https://production.onboardapp.io/api'
analytics:
  ga: '123456'
# More configuration keys for selected environment

HUB Client Core Introduction

HUB Client Core includes components with complex and sophisticated logic, that can`t be implemented in YAML.

Basics

Keep this in mind:

Project settings

Project settings can be set in the root index.yml file. The values for these settings can be individually set for any particular environment. (Environment-specific entry points)

List of available parameters

Parameter Required Description
analytics false Analytics configuration
authorizationTimeout false Authorization cookie expiration timeout
backDisabledAlert false Message to be displayed in case of disabled back action
coreLocale false List of translates for Core messages
defaultLocale false Default locale code
errorPage false Error page configuration
favicon false Path to favicon
flowName true Flow name in Backend instance
og true List of meta og tags
pages true Page Configuration
serverUrl true URL of Backend instance server
styles false LESS files includes
title true Meta title of an application
translates false List of translates for specific languages

Example configuration of index.yml

Here's an example of various settings in index.yml:

title: 'Zenoo Demo Project'
serverUrl: 'https://zenoo.onboardapp.io/api'
flowName: 'zenoo'
favicon: '/assets/favicon.ico'
styles:
  - !include ./styles/index.less
analytics:
  authorizationToken: !expression 'url.query.do_authorization'
authorizationTimeout: 60000
translates:
  en: !include ./translates/en.yml
  cz: !include ./translates/cz.yml
defaultLocale: 'en'
pages:
  index: !include ./pages/index.yml
  otp: !include ./pages/otp.yml
  loan-overview: !include ./pages/loan-overview.yml
  thanks: !include ./pages/thanks.yml
  rejected: !include ./pages/rejected.yml

Page settings

The entire application consists of pages. Each view that is presentable to a user must be implemented as page. There are two predefined user positions, the index page and the error page. The index must be inside index property in pages. In root of your yaml (typically index.yml), you can specify the value of the property errorPage. This property is name of page to which the user will be redirected when an error occurs (such as a network failure).

List of available parameters

Parameter Required Description
analytics false Analytics configuration
defaultAction false Default form submit action name
defaultActionParams false Default form submit action params
defaults false Default values for form fields
fadeAnimationBack false Use "fade" animation on back action
fadeAnimationSubmit false Use "fade" animation on submit action
items false Elements tree of specific page
og true List of meta og tags (will be merged with the ones coming from project configuration)
schema false Validation rules as a JSON schema
title false Page meta title

Example configuration

components:
  formLayout: !include @common/layouts/form-layout.yml
  formGroup: !include @common/components/form-group.yml
  header: !include @common/components/header.yml
  pinInput: !include @common/components/pin-input.yml
fadeAnimationBack: true
schema:
  required:
    - code
  properties:
    code:
      type: string
      minLength: 4
      maxLength: 4
  errorMessage:
    _: '{field} - Required field'
defaults:
  mobile: !expression 'flow.export.mobile'
items:
  - !ref components: formLayout
    items:
      - !ref components: header
        progress: <\%-((3 / 8) * 100)\%>
      - !component as: div
        className: 'content main-content'
        items:
          - !component as: h1
            items: 'Enter your phone number'
          - !component as: p
            items: 'Please enter a valid mobile phone number to where we can text a confirmation code to.'
          - !ref components: formGroup
            items:
              - !ref components: pinInput
                field: code
                label: 'Enter your confirmation code'
                length: 4

Error page

The error page can be specified as an errorPage parameter in application configuration.

errorPage: 'error-page'
---
pages:
  error-page: !include ./pages/error.yml # Include error page to a list of pages

If an error page is not specified, the auth cookie will be deleted and application will be reloaded.

You can create more dynamic error page that provides useful features, such as a button to continue or reattempt the previous action (flowContinue). This button will automaticaly fetch the last stored data from the server and redirect user to correct screen.

Another useful error management feature is to provide a button that reloads the flow. If the problem is not easily resolved, have the user click a button to redirect to start of the flow in case with the form action flowReload.

If you are on error page, there are also available page parameters that contain the reason for the error. For example, query the value of page.params.error to get the raw output from the error catch.

Analytics

Global application state and methods

Expressions

Expressions are a simple way to access data from the app runtime, or the response from server. Data is accessed through an object that is internally known as AppDataContext. If expression fails, it will return undefined. If you specify the default parameter, it will be returned when expression fails.

Examples of expressions:

property: !expression flow.export.value
property:
  !expression eval: flow.export.value
  default: Nothing

Functions

A function is another type of expression. It's useful to add some callbacks, such as a button on_click event. It's also necessary for creating a script snippet.

AppDataContext

flow: {
  export: ...any-data-from-server, // - this is exported data for page in flow from server
  backEnabled: true/false, // - value of backEnabled from server for current page
  function: {
      [function-name]: (payload?: any, resultKey?: string) => void, // - call (in !fuction) any flow/route function by call function name (like `flow.function.search('something')`), you can also set output resultKey (default function-name)
      results: {
          [function-name or result-key]: ...any-data-from-server-function, // - here will be data from server under function-name or result-key property name (like `flow.function.results.search`)
      }
  }
}
form: {
  data: {
      [field-name]: ...data-inside-field, // - data can be string, file, etc.
  },
  field: {
      [field-name]: {
          isValid: boolean,           // - is field valid
          validationError: string,    // - only validation erros generated by page schema
          error: string,              // - all field errors including validation errors and server errors
          isFilled: boolean,          // - is there any data
      }
  },
  valid: boolean,           // - is form valid
  disabled: boolean,        // - is form disabled
  tag: {
      [tag-name]: boolean,  // - form visual tags
  },
  // checks if all tags are present
  hasTags: (tags: string | string[]) => boolean
  // add tags to form
  addTags: (tags: string[]) => void,
  // regexp as string can be also used to identify more tags
  removeTags: (tags: string[]) => void,
  // change value of some field, you can use callback that will be called after data set, for example if you need to submit form
  changeValue: (fieldName: string, value: any, callback?: () => void) => void,
  // submit form
  submit: (actionName: string, params: string[]) => void,
}
app: {
  waiting: {
      [waiting-tag]: boolean, // - app waiting for tags
  }
}
api: {
  progress: {
      [field-name]: number // - percentage of progress in file uploading
  }
}
analytics: {
  authorization: (token) => void, // - trigger mixpanel.identify(token), GA.set({ userId: token }) and GTM dataLayer event "authorization" with parameter token (string)
  event: (eventName, eventParams) => void, // - trigger GTM dataLayer event with given eventName (string) and eventParams (object)
}
formatter: {
  thousandsSeparator: (value, separator) => string
}
financial: {
  rate: (periods, payment, present, future, type, guess) => any,
  ptm: (ir, np, pv, fv, type) => number,
  round: (num, places?) => number,
}
url: {
  ... // - https://github.com/unshiftio/url-parse
}
t: (string, params) => string
page: {
  params: any // - for example page.params.error contains informations, why you are on error page
}

Form

FormProvider is component that handles all changes to form input components. It also provides validation checks on the page schema. The form is submitted by the FormSubmit component. Every form submit must have specified action. The default action for this button is flowSubmit.

The entire page also has a default action, which occurs when the user attempts to submit a form by pressing the Enter key. Waiting screens can be also submited by implementing a FormAutosubmit component with the property actionName: flowSubmit—which automatically performs a submit when the the screen loads.

Built in form actions:

Translations

There are two ways to use translations:

To change locale, use the form action changeLocale, in which the first parameter is target locale name.

Examples:

# Evaluate translation for given translation key
- !t translation_key

# Evaluate translation for dymanic translation key (e.g. error coming from Backend)
- !expression t(flow.export.translation_key)

# Change locale
- !ref components: button
  text: Switch to English
  actionName: changeLocale
  actionParams: en

Built in components

Components

Each component must have an "as" parameter that specifies the component element name. You can use the provided component name, or the standard HTML Dom element.

Each component has also $reference property, which can create a named reference to DOM element. This reference is accessible through a $reference object inside appDataContext.

Examples of $reference:

# Referencable div
!component as: div
$reference: myDiv

# Some component that uses this reference
...some cother component
property: !expression #reference.myDiv

Debug cookies

In the hub-client, you can add the following debug cookies to enable verbose logging in a development tool:

Iterations

The !repeat expression provides a way to iterate on some collections and map components to it. You simply specify an array as the input collection (you can use an expression as well). Also specify a component that will be rendered n times (n is length of array).

Inside the component, you can access an item by expression that is stored in the key. In the expression, you can directly access an item of the array ([key].item) or its index ([key].index).

This is the format:

!repeat [key]: [array]
component: [some component]

Examples:

Here's an array from the api:

!repeat person: !expression flow.export.persons
component:
  !component as: span
  items: !expression person.item.name

Here's an array in yaml:

!repeat item:
  - Item 1
  - Item 2
component:
  !component as: span
  items: !expression item.item

Form components

FormInput

Implementation of standard HTML input with connection to FormProvider.

The name is required. Type has default value text. You can also specify a mask.

Propeties:

name: string;
className?: any;
autoComplete?: string;
type?: 'text' | 'password' | 'date' | 'number' | 'range';
max?: number | string;
maxLength?: number;
min?: number | string;
minLength?: number;
step?: number | string;
disabled?: boolean;
placeholder?: string;
mask?: string;
id?: string;
removeCharsRegExp?: string;
allowOffStep?: boolean;
showGuide?: boolean;
forceNumeric?: boolean;
selectAllOnFocus?: boolean;

Mask: Everything that is inside {} will be used as a regexp. Everything in [] will be escaped as plain text. Text written inside () will be escaped as well, but will be excluded from output. You can use macros within text that is not inside any bractets. Predefined macros are # (A-Za-z), A (A-Z), a (a-z), 9 (0-9).

You can also use predefined masks:

Examples:

[NUM]999/99        -> 3 digits, then "NUM" in plain text
[TEST]999{[\-]}999 -> 3 digits, "-" (defined as regexp), then 3 another digits
(ABC)99             -> ABC and 2 digits, but output will be only 2 digits
email()             -> Email mask
number(, 4, 2, Kč, true, '.')  -> 4 digits before dot, 2 digits after dot, "Kč" suffix, separate thousands, use dot as thousands separator


FormCheckbox

Properties:

name: string;
className?: any;
disabled?: boolean;
id?: string;


FormRadio

Properties:

name: string;
value: string;
className?: any;
disabled?: boolean;
id?: string;


FormTextarea

Properties:

name: string;
className?: any;
autoComplete?: string;
maxLength?: number;
minLength?: number;
disabled?: boolean;
placeholder?: string;
id?: string;
submitOnEnter?: boolean;

FormValue

Only shows data inside a field having this name.

Properties:

name: string;


FormHidden

This component sends data to a form field.

Properties:

name: string;
value: any;


FormSelect

Standard HTML select. If you want a special-style select, use FormEnumSelect.

Properties:

name: string;
className?: any;
autoComplete?: string;
submitActionOnEnter?: string;
placeholder: string;
options: {[value: string]: string};
disabled?: boolean;
id?: string;


FormEnumSelect

A select that is built from divs and spans-it's prepared for styling. Every element inside its structure has a className.

Stateless class names:

css-form-enum-select
css-form-enum-select-value
css-form-enum-select-value-placeholder
css-form-select-overlay
css-form-enum-select-list
css-form-enum-select-button

State class names:

css-form-enum-select-open
css-form-enum-select-closed
css-form-enum-select-list-open
css-form-enum-select-list-closed
css-form-enum-select-button-open
css-form-enum-select-button-closed

Properties:

name: string;
className?: any;
placeholder: string;
options?: {[value: string]: string};
disabled?: boolean;
id?: string;


FormFieldError

Shows error and also label for a specific field. If you set onlyLabel to true, then no errors will arise. If the error has a {field} tag, label will be inserted at this point.

Class names:

css-form-field-error
css-form-field-error-hidden
css-form-field-error-visible

Properties:

name: string;
label?: ReactNode;
className?: string;
onlyLabel?: boolean;


FormGlobalError

Displays global errors, such as those from the server or a runtime error.

Class names:

css-form-error

Properties:

className?: string;


FormAddressInput

Address input using Google geocoding for prompting addresses according to FormEnumSelect.

Stateless class names:

css-form-enum-select
css-form-enum-select-value
css-form-enum-select-list

State class names:

css-form-enum-select-open
css-form-enum-select-closed
css-form-enum-select-list-open
css-form-enum-select-list-closed

Properties:

name: string;
addressFieldName?: string;
className?: any;
placeholder?: string;
label?: string;
disabled?: boolean;
id?: string;
selectAllOnFocus?: boolean;


FormDateInput

Date input with popup. This uses react-datepicker.

Properties:

name: string;
id?: string;
placeholder?: string;
min?: string;
max?: string;
dateFormat?: string;
autoComplete?: string;
showMonthDropdown?: string;
showYearDropdown?: string;


FormDateStringInput

Date input with fixed format to YYYY-MM-DD

Properties:

name: string;
id?: string;
placeholder?: string;
autoComplete?: string;
className?: string;


FormFile

File input

Properties:

name: string;
className?: any;
disabled?: boolean;
id?: string;
autoSubmitAction?: string;
multiple?: boolean;
resizeToMax?: number; - resize image(s) to maximal size in bytes
}


FormImage

Display an image that is stored in form data, such as from file input.

Properties:

name: string;
className?: string;
id?: string;


FormMap

Google map component. This component permits the user to select a location on map. It stores a readable address in the (name) field and also storing address parts into another field (addressFieldName).

Properties:

name: string;
addressFieldName?: string;


FormPinInput

Pin input is set of inputs (number is specified by the length property). This input can accommodate digits only.

Properties:

name: string;
className?: any;
length: number;
id?: string;


FormSlider

Input with type range

Propeties:

name: string;
className?: any;
max?: number | string;
min?: number | string;
step?: number;
disabled?: boolean;
id?: string;


FormTagButton

Button for adding or removing form visual tags (if property remove is set to true).

Propeties:

tag: string | string[];
remove?: boolean;
disabled?: boolean;
className?: any;
id?: string;
notFocusable?: boolean;


Fragment

Fragment wrapper - this is a dummy component without any effect on the rendering. You can wrap a set of components into it to create a single component with children.


ServerOnly

Items inside will be rendered only if JS isn't working on the server.


BrowserOnly

Items inside will be rendered only if JS works, and only on the browser.


ClassNameWrapper

Wrapper for adding a class (or removing) to/from an element. You can use expressions to determine whether to show the class or not. The default wrapping element is div, but you can change it by setting the property element - tag name.

Class names are specified as name of properties, and enable it or disable by setting this property to true or false, respectively. It's the same as CX (classNames package). You can also specify animatedClassName and this class name will be added, when some class changes, and will be removed in the animation end event.

Properties:

element?: string;
classNames: {[name: string]: boolean};
animatedClassName?: string;


VisibilityWrapper

Wrapper for hiding and showing elements. Elements (items) inside it will be shown only if property visible is set to true, or property invisible is set to false.

Properties:

visible?: boolean;
invisible?: boolean;


LoadingWrapper

Wrapper for hiding and showing elements. Elemens inside (items) will be shown only if app is waiting for a tag—which is specified by the tags property. Thew tags property is an array of arrays and the logic is [ [a and b] or [c and d] ]. Waiting tags are not same as visual tags., since waiting tags can be added only in the react part of application.

Standart tags:

WAITING_API
PROCESSING_API

Properties:

tags?: string[][];
timeout?: number;
className?: string;
classNames?: {visible: string; hidden: string};


ElementStateWrapper

Wrapper for adding classes to wrapper element based on state of some input (or inputs) in it. It can be used for handling mouse over, mouse down, etc. The default selector is input, and the default element is div. You can specify your class name to be a specific state by setting it in classNames property.

Posible state names:

focused
keyDown
mouseDown
mouseOver
disabled
visited

Properties:

element?: string;
className: string;
classNames: {[name: string]: string};
selector?: string;


FormSetButton

Button for setting the value of a specific field. The value is optional, but if is not set it will change the value to null.

Properties

name: string;
actionName?: string;
actionParams?: string[] | string;
disabled?: boolean;
className?: any;
id?: string;
value?: any;


Script

A script component that runs a script, which is defined by the onEnter and onLeave property (use a !function expression). The onEnter script will be run immediately if the trigger property is not defined—or the value of the trigger property changes from false to true. The onLeave script will be run when a component unmounts (in case trigger has not been set), or when trigger changes from true to false.

You can also specify runOnUpdate and the script will run whenever trigger changes. If runOnUpdate is false or not set, both the onEnter and onLeave scripts will be run only once. You can also provide a URL to an external script.

Properties

trigger?: boolean;
url?: string;
runOnUpdate?: boolean;
onEnter: () => void;
onLeave: () => void;
attributes?: {[name: string]: string} // - html attributes, can by used only in combination with url


FormSlideSelect

This component is for creating a carousel select. You can specify children (or by using options). The user can select these by swiping left or right. It's a standard form field.

Example

!component as: FormSlideSelect
className: carousel
name: field
options:
  option1:
    !component as: div
    className: carouselContent
    style:
      background: red
  option2:
    !component as: div
    className: carouselContent
    style:
      background: green
  option3:
    !component as: div
    className: carouselContent
    style:
      background: blue

Properties

name: string;
className?: string;
options: {[name: string]: React.ReactElement};
continuous?: boolean;


Trigger

This component permits you to track changes on some value and retrieve that value with an expression. On loading (mounting) of this component, the load callback will be called with current value. If this value was changed, the change callback will be called with old and new values. Value variables are accessible in scope. For load function there is value, that referes to current value of the trigger. In a change callback, there is an oldValue and newValue-which contain the values before and after the change, respectively.

Example

- !component as: Trigger
   value: !expression form.data.mobile
   load: !function form.changeValue('otherFieldName', '08123456789')
   change: !function console.log("Data was changed", oldValue, newValue)

Properties

value: any;
load?: (value: any) => void;
change?: (oldValue: any, newValue: any) => void;
hysterezis?: number; // timeout for on change callback


RenderInRoot

This component permits you to render any component in the root scope. If you wrap your component in this component, it will render it's content only after renderingf the content in the remainder of the page.

Example

- !component as: RenderInRoot
   items:
       - !component as: h1
          items: 'Hello world'

Technology Stack

Code languages Frameworks
HUB Backend Groovy, Java 8 Spring 5, Project Reactor
HUB Client TypeScript 3 React, Node.js

Environment

Server requirements

The following requirements and assumptions are based on average production values (seen in typical installation).

Assumptions

Transaction: Client starts the flow and reaches the first decision point.

Transaction hours: 7:00 - 24:00 (17 hours in total)

Peak times: 3 times per day (7:00, 12:00, 20:00)

Transaction numbers:

Period Count
Per Month 150K - 210K
Per Day 5K - 7K
Per Peak 1667 - 2333
Per Hour 417 - 583

Average network in/out: 50-100 MB

HUB Backend

Number of CPUs min 2
RAM 4 GiB
Storage min 8 GiB
OS Linux base (AWS Linux, RedHat, CentOS)
Dependency OpenJDK 8, Mongo DB

HUB Client

Number of CPUs min 2
RAM 2 GiB
Storage min 8 GiB
OS Linux base (AWS Linux, RedHat, CentOS)
Dependency Nginx

Operations

Deployment Options

By pipeline

The HUB platform can be easily integrated to any CI & CD environment such as Jenkins, Circle CI and GitLab.

In typical scenarios, at the end of a green pipeline step, compiled artifacts are uploaded and started automatically.

Typical steps of a pipeline may consist of:

Build

Example backend commands:

./gradlew clean build # Build and run all tests
./gradlew bootJar # Create runnable HUB instance artifact with all dependencies

Example frontend commands:

npm i # Install NPM packages
npm run build:transunion:production #Build HUB client application

This step compiles the artifacts and runs unit/integration tests in an isolated environment (e.g.: Docker image).

Deploy to Stage

After a successful build step, all artifacts are uploaded to the staging environment together with their configuration.

Deploy to Production

After end-to-end testing and UAT, when the necessary approval is in place, the stable stage version in question is uploaded to the production environment.

Manually

In case the client side has security concerns or internal process related limitations, artifacts are delivered (through the Zenoo artifact repository or external file system) and deployed manually.

Deployment can be done on-premise, where the client has its own dedicated servers—or on-cloud such as AWS or Azure.

As long as server requirements are met, the HUB platform can be deployed to any cloud provider.

Deliverables

Operational Commands

Here are examples of deployment commands:

HUB Backend

java -jar hub-instance.jar --spring.config.location=application.yaml

HUB Client

Client application files are compiled as static files to be served by a Nginx web server.

Here's a common deployment scenario:

Step 1 - Locate static files on server:

/var/www/hub-client

Step 2 - Set the location under Nginx configuration:

server {
    listen 8080;
    server_name hub-client.onboardapp.io;
    location / {
      root /var/www/hub-client;
      index index.html;
    }
}

Step 3 - Restart the server

sudo service nginx restart

Scalability, Fault tolerance and High Availability

Logging & Monitoring

Integration to reporting & logging tools:

Security

Encryption

APIs use secure encrypted connections (HTTPS)

Authentication & Authorization

hub-client API secured using JWT tokens (For bearer authentication, see API security for more details)

Static Code Analysis (Code Vulnerability)

In each release, the HUB backend & client code is analyzed by a static code analyzer to indicate vulnerabilities and potential security breaches.

Examples

Simple Loan Application

Here we present an illustration of a simple workflow. The client selects which type of application (Loan or Credit Card) to complete and will see a "Success" page at the end of the workflow.

Version 1 — All commands are in one workflow script

simple-app.wf

values.applicationTypes << [
        "card": "Credit Card",
        "loan": "Loan",
]

route('Application Type') {
    namespace application
    uri '/application-type'
    validate {
        selectedType
    }
    export types: values.applicationTypes
}

match(application.selectedType == 'card') {
    route('Credit Card Application') {
        namespace application
        uri '/credit-card-application'
        validate {
            email
            personalId
            amount
        }
    }
}

match(application.selectedType == 'loan') {
    route('Loan Application') {
        namespace application
        uri '/loan-application'
        validate {
            email
            personalId
            amount
            term
            purpose
        }
    }
}

route('Successful') {
    uri '/successful'
    terminal()
}

Version 2 - DSL commands are separated to .init file to keep .wf easy to read.

simple-app.wf

values.applicationTypes << [
        "card": "Credit Card",
        "loan": "Loan",
]

route('Application Type')

match(application.selectedType == 'card') {
    route('Credit Card Application')
}

match(application.selectedType == 'loan') {
    route('Loan Application')
}

route('Successful')

simple-app.init

route('Application Type') {
    namespace application
    uri '/application-type'
    validate {
        selectedType
    }
    export types: values.applicationTypes
}

route('Credit Card Application') {
    namespace application
    uri '/credit-card-application'
    validate {
        email
        personalId
        amount
    }
}

route('Loan Application') {
    namespace application
    uri '/loan-application'
    validate {
        email
        personalId
        amount
        term
        purpose
    }
}

route('Successful') {
    uri '/successful'
    terminal()
}

Route DSL Examples

Terminal route

route("Finish") {
    uri "/finish"
    terminal()
}

Terminal route with a checkpoint

route("Rejected") {
    uri "/rejected"
    checkpoint()
    terminal()
}

Route with result validation

route("Basic Info") {
    uri "/basic"
    namespace client
    validate {
        firstname
        lastname
        address {
            street
            city { values "Prague", "Paris" }
            zip { regex ~/^[0-9]{5}(?:-[0-9]{4})?$/ }
        }
    }
}

Route exporting a static map

route("Select Product") {
    uri "/product"
    namespace product
    export  product1: "Product 1",
            product2: "Product 2",
            product3: "Product 3"
    validate {
        values "product1", "product2", "product3"
    }
}

Route exporting an attribute

delivery.address << [street: "Dejvicka 18", city: "Prague", zip: 12345]

route("Delivery address") {
    uri "/delivery"
    export address: delivery.address
}

Route registration

register {
    route("Delivery address") {
        uri "/delivery"
        export delivery.address
    }
}

route("Delivery address") {
    export client.address
}

Payload Validation Examples

Single mandatory field

validate {
    mobile
}

Mandatory field with regex validator

validate {
     mobile { regex ~/[0-9]{5}/ }
}

Mandatory field with value validator

validate {
     product { values "product1", "product2", "product3"}
}

Mandatory nested fields

validate {
    firstname
    lastname
    address {
        street
        city
        state
        zip
    }
}