Onboarding users to your Ledger

Almost every app that is deployed in dabl is meant to be used by multiple users and not just you (its creator).

Each of those users must sign up with their own account and start acting on behalf of a daml party (or parties) that get provisioned to them. This provisioning needs to happen automatically and in an ad-hoc fashion when the user signs up for the first time. What is more, each new party that enters the system needs to get properly onboarded in your daml app so that it gets the appropriate disclosures and permissions to perform actions.

This section outlines the best practices and patterns for onboarding newly registered users to your daml app.

The Login In with DABL button

Screenshot Placeholder

This is a button that if clicked will redirect the new user to sign up with their email and password (or social login) and then redirect them back to your app. This process is even more straightforward for users that already have a dabl account. The login button is essentially an html <a> tag with a hypertext reference to a login url.

<a href={`https://login.projectdabl.com/auth/login?ledgerId=${yourLedgerId}`}>Log In with DABL</a>

Where yourLedgerId is the ledger Id of the deployed ledger for your app. (Can be found under Ledger Settings). Since this id is not known until the ledger is created, you can fetch it dynamically and form the url using this handy js function:

const makeLoginUrl = () => {
    let host = window.location.host.split('.')
    const ledgerId = host[0]
    let loginUrl = host.slice(1)
    loginUrl.unshift('login')

    return loginUrl.join('.') + (window.location.port ? ':' + window.location.port : '')
        + '/auth/login?ledgerId=' + ledgerId
 }

A successful user authentication will redirect the user to the app url. This url will carry two extra query parameters: party and token.

https://yourLedgerId.projectdabl.com/?party=theUsersParty&token=theUsersJWT

Your frontend can then extract those parameters and store them in local storage so it can retrieve them later to make authenticated calls:

const urlParams = new URLSearchParams(url.search)
 const party = urlParams.get('party')
 localStorage.setItem('party', party)
 const token = urlParams.get('token')
 localStorage.setItem('token', token)

Listening for new users

When a user registers in your ledger, their name will appear in the Parties list under the Live Data tab.

Screenshot Placeholder

Your frontend can then create a contract that looks something like the one in DABLChat:

template UserSession
  with
    operator : Party
    user : Party
    userName : Text
  where
    signatory user
    key user : Party
    maintainer key

    controller operator can
      UserSessionAck: ...
        do
          -- Things required to onboard your new user (or simply return the original contract)
          ...

This template requires three pieces of information:

  1. The UserAdmin Party which is plays the role of the operator in DABLChat. Once you have deployed a website for your application, you can get the UserAdmin Party at https://${yourLedgerId}.projectdabl.com/.well-known/dabl.json.
  2. The user Party. This is available alongside the query paramter as listed above.
  3. The party's userName (optional). DABLChat uses the name from the token.

After a user logs in to a ledger and creates this contract, the contract gets disclosed to your UserAdmin party. All you have to do from there is to run automation as UserAdmin and listen to creations of your contract (Chat.UserSession in this example). When such a contract gets created, your bot will read the user's Party from the contract and kick off an onboarding workflow for it.

Your automation callback should look something like this:

@client.ledger_created('Chat.UserSession')
def invite_user_to_chat(event):
    return exercise(event.cid, 'UserSessionAck')

Note: You can configure this with a few clicks from the Automation tab

Screenshot Placeholder

The bot will simply blindly accept all UserSessions that it sees. You could put more sophisticated logic here if you want to prevent certain logins from succeeding.

daml 1.2
 module MyApp where

 template Operator
   with
     operator : Party
   where
     signatory operator

     key operator : Party
     maintainer key

     controller operator can
       nonconsuming InviteUser : ContractId UserInvitation
         with
           user : Party
           userName : Text
         do
           create UserInvitation with ..

This is nothing but a typical “Initiate - Accept” pattern that is further discussed in the official DAML docs

But how to create the Operator contract in the first place?

This can be done either manually in the dabl console or more conveniently by adding an extra callback function to your bot:

@user_admin.ledger_ready()
 def create_operator(event):
     logging.info('The User Admin bot is ready')
     res = user_admin.find_active('MyApp.Operator')
     logging.info(f'found {len(res)} MyApp.Operator contracts')

     if not res:
         logging.info(f'Creating Operator contract for {user_admin.party}...')
         return client.submit_create('MyApp.Operator', { 'operator': user_admin.party })
     else:
         logging.info(f'Operator {user_admin.party} is ready')

Similar to the user_admin.ledger_created decorator, the user_admin.ledger_ready will fire once when the bot has properly initialized. The create operator function will first search for any active contracts of template MyApp:Operator and if none exist (which will be the case if this gets deployed for the first time) it will go ahead and create one. Otherwise it will just print another log announcing that it is ready for requests.

Dealing with race conditions

What happens if a user logs in to your app before you have deployed your UserAdmin bot? Or what if you want to redeploy your bot and make sure that you can accommodate any user that signed up while your bot was down?

A fairly easy way to handle this would be to add recovery logic to your bot. Let’s look at this snippet below:

 @user_admin.ledger_created('MyApp.Operator')
  def invite_existing_users(event):
      logging.info(f'On MyApp:Operator created!')

      pending_sessions = user_admin.find_active('Chat.UserSession')

      return [exercise(cid, 'UserSessionAck') for cid in pending_sessions.keys()]

This could be used to make sure that any party that had logged in before you started your bot can be properly onboarded.

The moment a MyApp:Operator contract gets created, it will fetch all the UserSession contracts. It will then acknowledge each of them. Each UserSessionAck choice can lead to loging the user in or inviting them to join if it is their first time. In this example, all double logins and race conditions are handled in the DAML code.