- Overview
- Tutorials
- Getting started
- Get started with Canton and the JSON Ledger API
- Get Started with Canton, the JSON Ledger API, and TypeScript
- Get started with Canton Network App Dev Quickstart
- Get started with smart contract development
- Basic contracts
- Test templates using Daml scripts
- Build the Daml Archive (.dar) file
- Data types
- Transform contracts using choices
- Add constraints to a contract
- Parties and authority
- Compose choices
- Handle exceptions
- Work with dependencies
- Functional programming 101
- The Daml standard library
- Test Daml contracts
- Next steps
- Application development
- Getting started
- Development how-tos
- Component how-tos
- Explanations
- References
- Application development
- Smart contract development
- Daml language cheat sheet
- Daml language reference
- Daml standard library
- DA.Action.State.Class
- DA.Action.State
- DA.Action
- DA.Assert
- DA.Bifunctor
- DA.Crypto.Text
- DA.Date
- DA.Either
- DA.Exception
- DA.Fail
- DA.Foldable
- DA.Functor
- DA.Internal.Interface.AnyView.Types
- DA.Internal.Interface.AnyView
- DA.List.BuiltinOrder
- DA.List.Total
- DA.List
- DA.Logic
- DA.Map
- DA.Math
- DA.Monoid
- DA.NonEmpty.Types
- DA.NonEmpty
- DA.Numeric
- DA.Optional
- DA.Record
- DA.Semigroup
- DA.Set
- DA.Stack
- DA.Text
- DA.TextMap
- DA.Time
- DA.Traversable
- DA.Tuple
- DA.Validation
- GHC.Show.Text
- GHC.Tuple.Check
- Prelude
- Smart contract upgrading reference
- Glossary of concepts
Application structure¶
Example application architecture¶
Canton applications have three layers, User Interface, Local Business Logic and State, and Consensus Business Logic and State. These may look like the traditional 3-tier architecture of User Interface, Business Logic, and Database, but treating them this way results in underperforming applications that generate unnecessary traffic on the Global Synchronizer.
Don’t treat the blockchain as a database. While they have similar features, standard database design techniques are not optimized for blockchains. Instead, think about whether operations need consensus from multiple parties or can be handled locally by a single participant, in other words, think in terms of consensus vs. local state.
Local Business Logic and State: Actions and data that a single participant node can handle independently without consensus from others.
Consensus Business Logic and State: Actions and data that require agreement from multiple parties and must be handled using Daml smart contracts.
Frontend |
User Interface |
|
HTTP/JSON |
||
Backend Services |
Local Business Logic and State |
|
GRPC/Protobuf |
HTTP/JSON |
|
Daml Models |
Consensus Business Logic and State |
If your backend is full of CRUD operations, you’re treating the blockchain like a database. Canton synchronizes data between organizations with limited trust, so operations should represent business processes, not simple data updates.
Canton’s privacy guarantees require that business logic needing authorization or verification by multiple parties be implemented in Daml smart contracts. The Global Synchronizer coordinates consensus on authorization, verification, and visibility.
See the Daml Philosophy Course 2 “Daml Workflows” for details regarding the distinction between local vs. consensus logic and state.
Alternative application architecture¶
The Quickstart application uses backend services as a middle layer between the frontend and Daml models. This is known as a fully mediated architecture.
Frontend → Backend Services → Daml Models (for writes)
Frontend → Backend Services → Daml Models (for reads)
The backend “mediates” (acts as a middle layer for) every interaction between the frontend and the consensus layer.
Alternatively, the Quickstart application could have used a CQRS-style (Command Query Responsibility Segregation) architecture, where frontend actions bypass the backend and go directly (unmediated) to the consensus business operations.
Frontend → Daml Models directly (for writes)
Frontend → Backend Services → Daml Models (for reads)
In this example of a CQRS model, the user interface updates (writes) go directly to Daml models rather than being mediated through backend services. User interface queries (reads) still use backend services, which also handle external integrations and automation.
Frontend |
User Interface |
||
HTTP/JSON |
Ledger Update Operations |
||
Backend Services |
Queries, Automation and Integration |
||
GRPC/Protobuf |
HTTP /JSON |
||
Daml Models |
Consensus Business Logic and State |
For a detailed discussion on options for application architectures see the free courses in the Technical Solution Architect Certification.
Daml model structure¶
Let’s look at how the consensus business logic layer works in practice. The Quickstart uses a licensing application with two Daml modules, AppInstall and License. Util is infrastructure that contains helper functions that fall outside our area of interest. Each module handles different aspects of user onboarding and license management.
% tree licensing
licensing
├── daml
│ └── Licensing
│ ├── AppInstall.daml # User onboarding workflow
│ ├── License.daml # License creation and renewal
│ └── Util.daml # Shared utilities
└── daml.yaml
3 directories, 4 files
The core business flow requires consensus between multiple parties (the app-provider, app-user, and DSO), so it lives entirely in the consensus layer through these Daml smart contracts:
AppInstall.daml handles user onboarding through two templates: - AppInstallRequest - A user requests to install the app - AppInstall - The provider accepts, creating an active installation that can generate licenses
License.daml handles time-based access control with two templates: - License - Grants access for a specific time period - LicenseRenewalRequest - Handles license extensions through payments
These operations require consensus because they involve agreements between multiple parties.
For example, license renewal requires the user to pay, the provider to validate the payment, and the DSO to process the payment through its system. This multi-party coordination is why these operations belong in the consensus business logic layer, rather than the local backend services.
In Quickstart, the workflow follows this sequence: 1. User creates an AppInstallRequest (consensus - provider must see and respond to the request) 2. Provider exercises AppInstallRequest_Accept to create an AppInstall (consensus - both parties must agree) 3. Provider creates License contracts for specific features or time periods (consensus - user must accept terms) 4. License renewal involves payment validation across all three parties (consensus - user and provider require DSO’s payment system)
Each step requires multi-party agreement, which is why it belongs in the consensus business logic layer rather than being handled by local backend services.
Key Daml templates¶
AppInstallRequest contract¶
The AppInstallRequest
contract initiates the app user onboarding process by capturing a user’s request to install the application.
The contract gives the application provider control over application access to accept or reject installation requests.
This contract offers three choices that extend the Propose/Accept pattern to allow the user to cancel the request.
The AppInstallRequest_Accept
choice allows the provider to accept the request.
When the choice is executed, it creates a new AppInstall contract and makes the provider and user signatories.
The AppInstallRequest_Reject
choice allows the provider to decline the request.
It archives the request contract and also records metadata about why the request was rejected in the ledger exercise event.
The AppInstallRequest_Cancel
choice allows the user to withdraw their request any time before the provider accepts the contract.
AppInstall contract¶
The AppInstall
contract maintains the formal relationship between the provider and user.
It tracks installation status and manages license creation.
The contract has two choices, AppInstall_CreateLicense
and AppInstall_Cancel
.
AppInstall_CreateLicense
allows the provider to create a new license for the user.
When the CreateLicense
choice is exercised it creates a new License contract.
It also increments numLicensesCreated
to track how many licenses exist which is used to assign each license a license number.
Note: Daml smart contracts are immutable, so “incrementing” the counter results in archiving the current AppInstall
contract and creating a new one with the updated counter, within the same atomic transaction.
AppInstall_Cancel
lets the provider or user cancel the installation.
License Contract¶
The License contract is the on-ledger record supporting the core business case for the application.
One critical field is the expiresAt
field, which both determines the duration of the license’s validity,
and is used to ensure that neither actor can revoke (archive) the license contract before expiry.
The contract also has two choices:
License_Renew
can be exercised by the license provider.
It creates a Splice AppPaymentRequest
and a LicenseRenewalRequest
contract.
The former is a part of the Splice Wallet Application, and is used to request an amulet transfer.
The choice of amulet is made via the DSO party used in the AppInstall
contract.
The current deployment configuration results in this being Canton Coin; however,
there is nothing in the Daml model, or the backend code, that prevents a different amulet from being used.
The License_Expire
choice allows either party to archive an expired License contract.
Common OpenAPI definition¶
Daml models define the consensus between the App Provider, App User, and the DSO (amulet issuer). Once the models are in use, the frontend user interface needs to be able to query and interact with the resulting ledger. The usual pattern is to store and index the relevant slice of the ledger in the Participant Query Store and provide a set of query web services that provide business-oriented queries resolved against the PQS postgres database.
The architecture used by the example application also exposes a variety of HTTP endpoints that allow the frontend to exercise choices, providing a bridge between the frontend and the GRPC Ledger API. This allows the backend to centralise authentication and access control code.
This does necessitate defining an API between the back and front ends. For this example application, we have chosen to use OpenAPI. The API definition is in common/openapi.yaml. It uses GET to access the query services in the backend, and POST to execute choices on contracts identified by contract-id in the URL.
Note: The HTTP method semantics align appropriately with the requirements of the Daml operations and we call this a “JSON API”. However, it is not a pure ReST API and does use HATEOAS. As mentioned above, the blockchain should not be viewed as a database since the underlying state is not rows in a database, or objects in a datastore, either of which would be compatible with the CRUD-style semantics that emerge with most modern ReST tooling. Instead the architecture style used here is more akin to a sophisticated RPC mechanism, where Contract-ids and their underlying contract are nouns and can be represented as ReST resources. However, not only does this fail to capture the ongoing business entity that often outlives any single contract, it misses the fact that at the core of Daml are the authorized choices which are verbs and therefore do not play nicely with ReST assumptions.
Backend services structure¶
The example backend is a SpringBoot application, at the core of which are the API implementation classes in com.digitalasset.quickstart.service.
Most of this code is standard Java SQL-backed JSON-encoded HTTP web services. The code is divided into several modules under com.digitalasset.quickstart.*:
config: Standard SpringBoot @ConfigurationProperties based components.
security: Handles security related-demands including OAuth2, shared-secret auth support, roles, and access control.
service: Implements the openAPI endpoints, including read-only calls to PQS via the DamlRepository
spring component and GRPC calls to the relevant validator via the LedgerApi
spring component.
ledger: The main class here is LedgerApi which handles the details of calling the relevant GRPC endpoints required to submit Daml commands and other requests to the Canton Validator.
repository: Includes `DamlRepository`. A @Repository component providing business-logic level query and retrieval facilities against the ledger via PQS (the Participant Query Store).
pqs: The main class is Pqs, which provides data-model level query and retrieval. This encapsulates the necessary SQL generation and the JDBC queries against the PQS Postgres database.
utility: Miscellaneous utility code including JsonUtil for JSON encoding and decoding, and DamlCodeGen which provides access to the Daml model-generated Java classes.
Ultimately, the main recommendation embedded in this code is to orient the web-service API around a combination of queries and choice invocations. This is hopefully adequately demonstrated in the open API definition. Other than that, the usual web service engineering considerations apply: separation of concerns, DRY, and the importance of centralizing SQL generation and authentication mechanisms to ensure we address these security sensitive components only once.
Frontend interface structure¶
One property of the fully mediated architecture used in the example application is that by delegating all operations to the backend, the open API schemas act as DTO (Data Transfer Object) definitions for the front and back ends. In simple cases, such as the example application, these can double as frontend models when using React, MVVM, FRP, or a similar frontend architecture style.
The CQRS alternative architecture does not use DTOs. Instead the backend services return Daml contracts directly. These are then generally deserialised directly into Javascript or Typescript objects, generated directly from the DAR files; and, used to populate the underlying frontend model. This direct coupling from Daml to Frontend can significantly simplify the code required for applications with requirements defined in terms of a Daml model. The mediated architecture is more suitable where the Frontend needs to incorporate sources of data additional to the Canton Ledger.
The example application is a naive React web frontend written in Typescript. It accesses the backend web services using the generator-less Axios client to handle the lowest-level transport, configured in src/api.ts:
import OpenAPIClientAxios from 'openapi-client-axios';
import openApi from '../../common/openapi.yaml';
const api = new OpenAPIClientAxios({
definition: openApi as any,
withServer: { url: '/api' },
});
api.init();
export default api;
Authentication is handled using OAuth2 against a mock OAuth server to perform the login; and, bearer tokens to identify the frontend to the backend. The frontend does not have any knowledge of Canton or Daml users or parties, this is delegated entirely to the backend.
The records defined by the OpenAPI definition are used directly as the models maintained within the React stores, and from there to the views via the usual React handlers.
Tooling in the Quickstart¶
For testing and experimentation there is a make target to create the AppInstallRequest
on behalf of the app user party.
.PHONY: create-app-install-request
create-app-install-request: ## Submit an App Install Request from the App User participant node
docker compose -f docker/app-user-shell/compose.yaml \
$(DOCKER_COMPOSE_ENVFILE) run --rm create-app-install-request || true
This uses curl via a utility function curl_check to submit a Daml Create command to Org1’s participant node via its HTTP Ledger JSON API (v2/commands/submit-and-wait).
% cat docker/app-user-shell/scripts/create-app-install-request.sh
#!/bin/bash
...
source /app/utils.sh
create_app_install_request() {
curl_check "http://$participant:7575/v2/commands/submit-and-wait" \
"$token" "application/json" \
--data-raw '{
"commands" : [
{ "CreateCommand" : {
"template_id": "#quickstart-licensing:Licensing.App Install:AppInstallRequest",
"create_arguments": {
"dso": "'$dsoParty'",
"provider": "'$appProviderParty'",
"user": "'$appUserParty'",
"meta": {"values": []}
}
} }
]
}'
}
create_app_install_request "$LEDGER_API_ADMIN_USER_TOKEN_APP_USER" \
$DSO_PARTY $APP_USER_PARTY $APP_PROVIDER_PARTY participant-app-user
Running this and then using Daml Shell (make shell provides a useful shortcut) to inspect the result on the ledger.
% make shell
docker compose -f docker/daml-shell/compose.yaml --env-file .env run \
--rm daml-shell || true
Connecting to
jdbc:postgresql://postgres-splice-app-provider:5432/scribe...
Connected to
jdbc:postgresql://postgres-splice-app-provider:5432/scribe
postgres-splice-app-provider:5432/scribe> active
┌─────────────────────────────────────────────────────────────┬──────────┬───────┐
│ Identifier │ Type │ Count │
╞═════════════════════════════════════════════════════════════╪══════════╪═══════╡
│ quickstart-licensing:Licensing.AppInstall:AppInstallRequest │ Template │ 1 │
├─────────────────────────────────────────────────────────────┼──────────┼───────┤
│ splice-amulet:Splice.Amulet:ValidatorRight │ Template │ 1 │
├─────────────────────────────────────────────────────────────┼──────────┼───────┤
│ splice-wallet:Splice.Wallet.Install:WalletAppInstall │ Template │ 1 │
└─────────────────────────────────────────────────────────────┴──────────┴───────┘
postgres-splice-app-provider:5432/scribe 3f → 42> active
quickstart-licensing:Licensing.AppInstall:AppInstallRequest
┌─────────┬──────────┬───────────┬───────────────────────────────────────────────┐
│ Created │ Contract │ Contract │ Payload │
│ at │ ID │ Key │ │
╞═════════╪══════════╪═══════════╪═══════════════════════════════════════════════╡
│ 42 │ 0058df2 │ │ dso: DSO: :1220c93d1... │
│ │ 3a5aaa4 │ │ meta: │
│ │ c2a53a...│ │ values: │
│ │ │ │ user: Org1: :12203a9a7... |
│ │ │ │ provider: AppProvider: :122030b08cfebb8c8... │
└─────────┴──────────┴───────────┴───────────────────────────────────────────────┘
postgres-splice-app-provider:5432/scribe 3f → 42> contract
0058df23a5aaa4c2a53aab496d12fb9e8ee74fb91614e5f7d50670598e4760eb23ca101220cc241620b310c93af45b2cd7cea7518e18e26f73f227813fec2bf4ea0bd69b940120cc241620b310c93af45b2cd7cea7518e18e26f73f227813fec2bf4ea0bd69b94
╓───────────────────────╥─────────────────────────────────────────────────────────────╖
│ Identifier ║ quickstart-licensing:Licensing.AppInstall:AppInstallRequest ║
╟───────────────────────╫─────────────────────────────────────────────────────────────╢
│ Type ║ Template ║
╟───────────────────────╫─────────────────────────────────────────────────────────────╢
│ Created at ║ 42 (not yet active) ║
╟───────────────────────╫─────────────────────────────────────────────────────────────╢
│ Archived at ║ <active> ║
╟───────────────────────╫─────────────────────────────────────────────────────────────╢
│ Contract ID ║ 0058df23a5aaa4c2a53a... ║
╟───────────────────────╫─────────────────────────────────────────────────────────────╢
│ Event ID ║ #12201612fb8a071e27ec...:0 ║
╟───────────────────────╫─────────────────────────────────────────────────────────────╢
│ Contract Key ║ <not set> ║
╟───────────────────────╫─────────────────────────────────────────────────────────────╢
| Payload ║ dso: DSO: :1220c93d13220b07f0e9a0a0f7a2381191d3bf3d21... │
| ║ meta: │
| ║ values: │
| ║ user: Org1: :12203a9a79d8f72b8cce37813713af7a51296def8... │
| ║ provider: AppProvider: :122030b08cfebb8c87c16793cba3783... │
╚═══════════════════════╩═════════════════════════════════════════════════════════════╝
postgres-splice-app-provider:5432/scribe 3f → 42>
Exercising the AppInstallRequest_Accept choice completes the onboarding. The frontend UI provides a way to do this.