Continuing with the 2-part series that is designed to guide developers as they attempt to ROFLize an app, we have so far covered the workflow, the prerequisites needed to proceed, how to containerize the app, and initialize.
In this concluding part of the series, I will show you how to create, build, deploy, and test the app.
Create
The first step for building an app is to create on-chain and assign a unique identifier or app ID. This ID is for use by on-chain smart contracts and has two functions:
- helps interact with the right app
- gives the app access to a decentralized key management system
To create an app, you will need enough funds. Currently, the threshold for creating a ROFL app is 100 tokens.
If you are doing this on mainnet, you will need to procure 100 ROSE from a centralized exchange.
For this tutorial, we are assuming a testnet setup, so you can get your 100 TEST free from the official faucet.
It is also advisable that you create or import a secp256k1-bip44 account to use with the Ethereum-compatible tooling like Hardhat.
The next step is to select the network - mainnet or testnet, and also the initial administrator account of the app, say myaccount. The CLI will automatically update the manifest file with the assigned app identifier.
oasis rofl create --network testnet --account myaccount
Once done, the CLI will also output the new identifier. For example:
Created ROFL application: rofl1qqn9xndja7e2pnxhttktmecvwzz0yqwxsquqyxdf
This makes the app deployer account the default initial admin for any update to the app's configuration. You can always change the admin address at any point.
Note: The CLI implements a simple governance mechanism. Here, the app's admin is a single account, and even a smart contract can be the admin. It is also possible to implement advanced agent governance mechanisms like using multi-sigs or DAOs with veto powers to control the upgrade process.
Now, a final note about the App ID. It is derived using one of the two schemes:
- Creator address + creator account nonce: This is the default. It helps run tests, e.g. in sapphire-localnet, where you want a deterministic app ID.
- Creator address + block round number + index of the rofl.Create transaction in the block: This is when you want a non-deterministic approach. It is the preferred mode in production environments. It deters potential attackers from determining the app ID in advance, even if they knew the creator address.
To select which app ID derivation scheme you want, you can simply pass the --scheme parameter.
Build
This operation brings together compose.yaml, specific operating system components, and the hash of a trusted block on the Sapphire chain. As a result, you can safely execute the app inside a TEE.

Whenever any part of the app changes, this step is necessary before deploying. The build process takes the compose file together with other ROFL artifacts and deterministically generates a bundle that you can deploy later.
Another function of the build process is to compute the enclave identity of the bundle. This is needed during remote attestation to authenticate the app instances before they can be allowed to access the key management system and other features.
oasis rofl build
Running this step generates a ROFL bundle to be used later for deployment.
ROFL app built and bundle written to 'myapp.default.orc'.
Update On-chain App Config
Any changes to the app's policy defined in the manifest also require updating the on-chain app config, so that the changes can take effect.
The designated admin account can issue an update transaction by running the CLI.
oasis rofl update
Alternatively, you can automate the build and verify steps using the build-deploy-rofl-action GitHub Action. More on this later.
Deploy
This is one of the most detailed sections of the workflow.
A ROFL app can be deployed to Sapphire or Cipher paratimes where the ROFL module has been installed. For our purposes, we will proceed with Sapphire, as it implements all ROFL functionalities.
You can deploy your app in several ways to a ROFL node, defined as a light Oasis Node with support for TEE and configured Sapphire runtime. I will discuss in detail on using the ROFL marketplace and GitHub Actions options.
Alternatively, you can copy over the ROFL bundle to your ROFL node manually and configure it. As the ROFL node is a separate and elaborate topic, not in the scope of this tutorial, we will not go into details here, so check out the quick guide for hosting the ROFL bundle directly.
Deploy on ROFL Marketplace
The Oasis CLI has built-in support for renting a machine on the ROFL marketplace and deploying an app. You can get a list of the default Oasis-managed ROFL providers with this command.
oasis rofl deploy --show-offers
Using provider: oasis1qp2ens0hsp7gh23wajxa4hpetkdek3swyyulyrmz (oasis1qp2ens0hsp7gh23wajxa4hpetkdek3swyyulyrmz)
Offers available from the selected provider:
- playground_short [0000000000000001]
TEE: tdx | Memory: 4096 MiB | vCPUs: 2 | Storage: 19.53 GiB
Price: 5.0 TEST/hour
If you want to select a different provider and offer, you can do so by using the --provider and --offer parameters, respectively.
For this tutorial, let us proceed with the default.
oasis rofl deploy
Using provider: oasis1qp2ens0hsp7gh23wajxa4hpetkdek3swyyulyrmz (oasis1qp2ens0hsp7gh23wajxa4hpetkdek3swyyulyrmz)
Pushing ROFL app to OCI repository 'rofl.sh/0ba0712d-114c-4e39-ac8e-b28edffcada8:1747909776'...
No pre-existing machine configured, creating a new one...
Taking offer: playground_short [0000000000000001]
The result of this command is threefold.
- It copies over the ROFL bundle .orc to an Oasis-managed OCI repository rofl.sh
- It pays offer playground_short with ID 0000000000000001 to provider oasis1qp2ens0hsp7gh23wajxa4hpetkdek3swyyulyrmz
- It returns the machine ID and stores it in the manifest file
You can check the status of your active ROFL machine.
oasis rofl machine show
Name: default
Provider: oasis1qp2ens0hsp7gh23wajxa4hpetkdek3swyyulyrmz
ID: 00000000000000a2
Offer: 0000000000000001
Status: accepted
Creator: oasis1qpupfu7e2n6pkezeaw0yhj8mcem8anj64ytrayne
Admin: oasis1qpupfu7e2n6pkezeaw0yhj8mcem8anj64ytrayne
Node ID: bOlqho9R3JHP64kJk+SfMxZt5fNkYWf6gdhErWlY60E=
Created at: 2025-05-22 15:01:47 +0000 UTC
Updated at: 2025-05-22 15:01:59 +0000 UTC
Paid until: 2025-05-22 16:01:47 +0000 UTC
Proxy:
Domain: m162.test-proxy-a.rofl.app
Ports from compose file:
5678 (frontend): https://p5678.m162.test-proxy-a.rofl.app
Resources:
TEE: Intel TDX
Memory: 4096 MiB
vCPUs: 2
Storage: 20000 MiB
Deployment:
App ID: rofl1qpjsc3qplf2szw7w3rpzrpq5rqvzv4q5x5j23msu
Metadata:
net.oasis.deployment.orc.ref: rofl.sh/0ba0712d-114c-4e39-ac8e-b28edffcada8:1747909776@sha256:77ff0dc76adf957a4a089cf7cb584aa7788fef027c7180ceb73a662ede87a217
Commands:
<no queued commands>
The details of the machine will show:
- Machine status and expiration date
- Provider information
- Proxy URLs for any published ports
- Resource allocation (TEE type, memory, CPUs, storage)
- Deployment details
To view the logs of the app, you need to use this command and sign the request with the app's admin account.
oasis rofl machine logs
Although only the app admin can access the logs, the data here is stored unencrypted on the ROFL node. So, during the production stage, you should never print any confidential data to the standard or error outputs.
GitHub Actions
We briefly touched on this during the build process. This is applicable for ROFL apps from CI/CD pipelines. Recap of the build-deploy-rofl-action automation.
Validate
You can identify configuration errors on every push and pull request with this command, and you don't need to run a full build.
- uses: oasisprotocol/build-deploy-rofl-action@master
with:
network: testnet
only_validate: true
Build and Verify
You can verify that your build is reproducible and that enclave IDs match the on-chain state. Any mismatch will fail the build.
- uses: oasisprotocol/build-deploy-rofl-action@master
with:
network: mainnet
skip_update: true
skip_deploy: true
Full Deployment
- uses: oasisprotocol/build-deploy-rofl-action@master
with:
network: mainnet
wallet_account: deployer
wallet_import: true
wallet_secret: ${{ secrets.WALLET_SECRET }}
wallet_algorithm: secp256k1-raw
Now, it is time to check that the app is running. It does not matter which deployment path you take; the command is the same.
oasis rofl show
App ID: rofl1qqn9xndja7e2pnxhttktmecvwzz0yqwxsquqyxdf
Admin: oasis1qrydpazemvuwtnp3efm7vmfvg3tde044qg6cxwzx
Staked amount: 10000.0
Policy:
{
"quotes": {
"pcs": {
"tcb_validity_period": 30,
"min_tcb_evaluation_data_number": 17,
"tdx": {}
}
},
"enclaves": [
"z+StFagJfBOdGlUGDMH7RlcNUm1uqYDUZDG+g3z2ik8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"6KfY4DqD1Vi+H7aUn5FwwLobEzERHoOit7xsrPNz3eUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
],
"endorsements": [
{
"any": {}
}
],
"fees": 2,
"max_expiration": 3
}
=== Instances ===
- RAK: AQhV3X660/+bR8REaWYkZNR6eAysFShylhe+7Ph00PM=
Node ID: DbeoxcRwDO4Wh8bwq5rAR7wzhiB+LeYn+y7lFSGAZ7I=
Expiration: 9
This shows that there is a single instance of the app running on the given node. It also displays its public runtime attestation key (RAK) and the epoch at which its registration will expire if not refreshed. So, if you want to continue working with the app, you will need to periodically refresh the registration.
You can also check out the status of your app on the Oasis Explorer → Sapphire → ROFL.
Mainnet
Testnet
Test
While discussing the initialization phase, I outlined several ways to do so. So, the testing process will vary based on which option you took.
TDX ROFL containers
No appd and Localnet Required
If your app doesn't rely on appd, you can export any secrets required by compose.yaml and execute podman compose up with a simple command.
export SECRET=some_secret
podman compose up --build
Testing with appd and Localnet
If your app is connected to a functional appd, you will need to spin up the sapphire-localnet Docker image and bind-mount the folder that contains your rofl.yaml.
docker run -it -p8544-8549:8544-8549 -v .:/rofls ghcr.io/oasisprotocol/sapphire-localnet
It will run for a few moments and give an output like this.
* TDX ROFL detected. Localnet appd service will be accessible via UNIX socket and TCP port
UNIX socket (Linux)
Shell
curl --unix-socket ./rofl-appd.sock http://localhost/rofl/v1/app/id
TCP port (macOS, Linux)
Shell
curl http://localhost:8549/rofl/v1/app/id
ROFL clients can connect to the TCP port using one of three ways.
Python
from oasis_rofl_client import RoflClient
client = RoflClient(url="http://localhost:8549")
TypeScript
import { RoflClient } from '@oasisprotocol/rofl-client';
const client = new RoflClient({ url: 'http://localhost:8549' });
Rust
use oasis_rofl_client::RoflClient;
let client = RoflClient::with_url("http://localhost:8549")?;
Once all this is done, you can now spin up your services and run tests. Usually we use the same compose.yaml for production and testing. So, the following command is useful to point a service to an alternate Localnet's rofl-appd.sock location.
compose.yaml
volumes:
- ${ROFL_APPD_SOCKET:-/run/rofl-appd.sock}:/run/rofl-appd.sock
Now, when you execute podman compose, it brings up your services in the Localnet environment.
export SECRET=some_secret
export ROFL_APPD_SOCKET=./rofl-appd.sock
podman compose up --build
In the final step, you are now ready to try out the app or execute your end-to-end integration tests as you see fit. Another option would be to use sapphire-localnet in the form of a GitHub action inside your CI service.
TDX ROFL raw
Local testing for this is currently not supported. So, if you have initialized with this option, you will need to deploy them on Sapphire Testnet and try them out.
SGX ROFL
To test this, the first step is to compile your SGX ROFL with debug option enabled. Invoke this command to create a separate localnet deployment in your ROFL manifest.
rofl.yaml
deployments:
localnet:
app_id: rofl1qqn9xndja7e2pnxhttktmecvwzz0yqwxsquqyxdf
network: testnet
paratime: sapphire
admin: test:bob
debug: true
oasis rofl build --deployment localnet --offline
Running this command produces a bundle named your-app-name.localnet.orc.
Now, when you execute the sapphire-localnet Docker image and bind-mount the folder that contains the ROFL manifest, the image will automatically register the ROFL on Localnet and execute it inside the Localnet ROFL node.
docker run -it -p8544-8548:8544-8548 -v .:/rofls ghcr.io/oasisprotocol/sapphire-localnet
To verify that the Localnet .orc bundle was loaded and you can test, look out for this output.
* Detected SGX ROFL bundle: /rofls/rofl-appd-localnet.localnet.orc
This concludes the detailed workflow of how to ROFLize an app. However, other aspects of this process also deserve a tutorial. For example, you may wonder what features a ROFL app has or how to troubleshoot, should you need to. I will write up a bonus guide for these topics next time.
For a quick chat with the Oasis engineering team for help with specific issues, you can drop your comments in the dev-central channel in the official Discord.
Top comments (0)