Skip to content

Anatomy & Lifecycle

The single most important idea: 1 Smart App = 1 DKG entity = 1 subscription NFT. You mint that identity once (by subscribing + paying); from that instant the app is ACTIVE. Whether or not a container is running is a different question, tracked on a different axis. Knowing those two axes apart is the difference between “it deployed” and “I know why it deployed.”


Two documents describe a Smart App. You write the deploy manifest; the CLI writes the app config for you when you subscribe.

hsuite deploy and hsuite redeploy read a JSON manifest. This one comes from the Smart App boilerplate:

smart-app.manifest.json
{
"name": "boilerplate",
"services": ["database", "storage"],
"degree": 3,
"backend": {
"port": 3200,
"tag": "v1",
"replicas": 1,
"context": ".",
"dockerfile": "Dockerfile",
"env": { "NODE_ENV": "production", "PORT": "3200" }
},
"frontend": {
"build": "cd ../../ionic-dapp && npm run build -- --output-path ../smart-app/public",
"dir": "public"
}
}
Field Meaning
name Required. The app’s name.
services Required. The BaaS services the app uses: auth, database, storage, functions, messaging.
degree Optional. An integer of at least 3.
backend.port Required. The port your backend listens on inside the container. 8080 is reserved.
backend.tag Required. The image tag. Tags are immutable: every new version needs a fresh one.
backend.replicas Optional, default 1.
backend.context, backend.dockerfile, backend.platform, backend.build Optional. How the CLI builds the image.
backend.env Optional. Non-secret settings. A non-empty value in your --env file wins over the same key here.
backend.limits Optional. CPU and memory limits.
backend.egress Optional. External hosts the app asks to reach ({ "host", "port" }, at most 50). Outbound traffic is blocked by default.
frontend.build, frontend.dir, frontend.bundlePath Optional. A frontend bundle to build and upload with the deploy.

hsuite subscribe signs and sends the app config with the create request. The cluster stores it and pins it to IPFS. Its fields:

Field Meaning
appId Replaced by the DKG entityId when the app is created.
name The app’s display name.
acceptedLoginChains The chains your customers can sign in with. The CLI sets ['xrpl'].
acceptedPaymentTokens Stored with the config. See TokenGate.
customerScope strict (the default) or permissive: whether the platform refuses a data-plane call from your app that carries no customer identity. Change it with hsuite customer-scope.

The validators keep a record of each app. Its main fields:

Field Meaning
appId Stable identifier — equal to the DKG entityId and the subscription appId
developerWallet / developerChain Who owns and pays for it (taken from your signed session)
state Lifecycle — is this app allowed to run? One of PENDING_SUBSCRIPTION, ACTIVE, SUSPENDED, RETIRED
runtime.runtimeState Runtime — is it actually running? Independent of state. One of NOT_DEPLOYED, DEPLOYING, RUNNING, FAILED, DEGRADED
configCid IPFS CID of the app config
xrplSignerListAddress The app’s XRPL multisig account
subscriptionId / subscriptionNftSerial The paid subscription and its NFT
activatedAt When the app became ACTIVE (the NFT mint)
listing Present once you publish the app to the marketplace (listed, trustScore, featured, subOffer, includedAppIds, …). See Listing your app on the marketplace.
runtime.env Your env values, each sealed in a post-quantum hybrid envelope (see Secrets)

Step 1 — Mint: the subscription ceremony (hsuite subscribe)

Section titled “Step 1 — Mint: the subscription ceremony (hsuite subscribe)”

This is where the app is born ACTIVE. hsuite subscribe sends a create request signed by your wallet (POST /api/v3/smart-apps). The validators run the per-app DKG ceremony and create the paid, deposit-backed subscription on that same DKG entityId. When the deposit confirms, they mint the subscription NFT and the app turns ACTIVE. No second mint, no throwaway id, no fork.

sequenceDiagram
    autonumber
    participant Dev as Your CLI / SDK
    participant V as Validators
    participant IPFS as IPFS
    participant Chain as XRPL

    Dev->>V: create request (signed create intent + rule)
    V->>V: per-app DKG (keys exist only as shares)
    V->>Chain: create the app's multisig account
    V->>IPFS: pin the app config
    IPFS-->>V: configCid
    V->>V: bind your signed rule
    Note over V: state = PENDING_SUBSCRIPTION
    V-->>Dev: appId == entityId
    Dev->>Chain: pay the subscription deposit (HST)
    Note over V: deposit confirms, NFT mints
    Note over V: state = ACTIVE
    V->>V: every validator verifies and stores the app record

The only way to ACTIVE is a confirmed deposit followed by the NFT mint: no path stores an ACTIVE app without a minted, deposit-backed subscription. Every validator verifies the signed creation event and keeps its own copy of the app’s record. Receiving the same event twice changes nothing.


Step 2 — Deploy: attach runtime to the already-ACTIVE app (hsuite deploy)

Section titled “Step 2 — Deploy: attach runtime to the already-ACTIVE app (hsuite deploy)”

Deploy is the later step. It does not create a DKG entity, does not mint a subscription, and does not allocate an appId — those already exist from mint. hsuite deploy takes the existing SUBSCRIPTION_APP_ID as its target, fetches push credentials for your image, pushes it, and the platform starts your container. It moves runtime.runtimeState, never state.

The host checks that the app exists, that you own it and that it isn’t retired. Without an active subscription it refuses to issue push credentials, with 402.


Your container creates and registers nothing at boot: the app’s identity was fixed at mint. Its id (appId == entityId), its rule and its owner come from the create intent your wallet signed in hsuite subscribe. POST /api/v3/smart-apps refuses a create without that intent (400), the id is derived from it, and every validator refuses to take part in an app creation that carries none, so no single validator can create your app under an id or a rule you did not sign.

The running code acts as that app: the SDK is configured with its appId (SmartEngineServiceConfig.appId, from @hsuite/smart-engines-sdk/nestjs). The heavy lifting (identity, rules, on-chain operations) stays on the validators, so your app stays thin.


A Smart App lives on two independent axes. Conflating them is the classic mistake.

Lifecycle state — is this app allowed to run?

Section titled “Lifecycle state — is this app allowed to run?”
stateDiagram-v2
    [*] --> PENDING_SUBSCRIPTION : subscribe (DKG + deposit pending)
    PENDING_SUBSCRIPTION --> ACTIVE : deposit confirmed then NFT minted
    ACTIVE --> SUSPENDED : you suspend or subscription expires
    SUSPENDED --> ACTIVE : resume / renew
    ACTIVE --> RETIRED : retire (tear down)
    SUSPENDED --> RETIRED : retire
    RETIRED --> [*]
Transition Trigger
PENDING_SUBSCRIPTION → ACTIVE The deposit confirms and the subscription NFT mints. This is the only way to become ACTIVE, on testnet as on mainnet.
ACTIVE → SUSPENDED You suspend (hsuite suspend), or the subscription expires.
SUSPENDED → ACTIVE Resume (hsuite resume) or renew.
ACTIVE / SUSPENDED → RETIRED Tear-down, gated on an owner-signed retire proof (see Retiring an app). RETIRED is terminal; the host refuses to re-issue credentials for a retired app. A lapsed subscription does not block retire — you can always tear an app down.

Runtime runtime.runtimeState — is a pod actually up?

Section titled “Runtime runtime.runtimeState — is a pod actually up?”
stateDiagram-v2
    [*] --> NOT_DEPLOYED : ACTIVE but no runtime yet
    NOT_DEPLOYED --> DEPLOYING : hsuite deploy
    DEPLOYING --> RUNNING : container healthy
    DEPLOYING --> FAILED : reconcile failed
    RUNNING --> DEGRADED : partial health
    DEGRADED --> RUNNING : recovers
    RUNNING --> NOT_DEPLOYED : suspend / retire drains it

An ACTIVE app sits at NOT_DEPLOYED until you deploy. The host tier (see Deployment) moves this axis; the validators own the lifecycle axis above.


Retiring (tearing down) an app is a privileged, owner-authorized action — more than just being logged in. The validator refuses to retire unless the request carries an owner-signed retire proof: a short, domain-separated message your developer wallet signs.

The signed message is a retire-intent with a fixed shape:

Field Meaning
appId The app being retired.
action Always 'retire'.
developerWallet / developerChain Your wallet — must match the app’s registered owner.
nonce A single-use random value. A replayed nonce is refused, even if the app is somehow forced back to a retirable state.
iat / exp Issued-at / expiry. The window is capped at 10 minutes — an old proof can’t be stockpiled.

The validator verifies the signature against the app’s registered (developerWallet, developerChain) with the same wallet verifier your sign-in uses, then embeds the proof in the signed retirement event. Every service that acts on the retirement can therefore confirm the owner really asked for it. A single compromised or faulty validator cannot retire (and eventually erase) an app it doesn’t own.

You don’t assemble this by hand. hsuite delete and the SDK’s client.deployment.delete(appId, signer) build the retire-intent, sign it with the wallet key you already authenticate with, and attach it to the DELETE /api/v3/baas/deployment/apps/:appId call for you.

A lapsed subscription never blocks retire. The active-subscription gate guards paid-usage surfaces (deploy, attach a frontend, list on the marketplace). Asking to stop paying and tear the app down is not usage, so retire is allowed on an ACTIVE, SUSPENDED, or unpaid PENDING_SUBSCRIPTION app alike. Ownership plus the signed proof are the only gates.


You do not HTTP-trigger validator-side work after submitting. Once your entities exist, the cluster watches the relevant on-chain state for the entities it governs and acts on its own, over its internal event bus. Deposit detection, the PENDING_SUBSCRIPTION → ACTIVE mint transition, lifecycle events — all driven by watchers, not by your app calling home.


Minting and deploying give you a running app. Listing Optional is a third, entirely optional step: publishing that ACTIVE app to the public marketplace so other wallets can discover it and subscribe. Listing is its own axis — it neither mints nor deploys, and an app is perfectly happy ACTIVE-and-running while never listed.

The listing lives on the app record’s listing field and only appears in the public marketplace when all of these hold: the app is state: ACTIVE, it has a listing, listing.listed === true, and the listing fee is paid up. Anything else is invisible to browsers — a suspended, unpaid, or never-listed app simply doesn’t show up.

Every listing write — publish, edit, feature, bundle — is owner-scoped. The validator checks that the app’s owner is your authenticated wallet before any write or any fee wallet is opened, so no one can list, feature, or re-compose an app they don’t own.

Publishing is a two-beat “declare, then pay” ceremony against the validator’s owner-scoped surface (POST /api/v3/smart-apps/marketplace/apps/:appId/publish):

flowchart TB
    Owner["You (app owner)"] -->|"publish listing metadata"| Floor{"Anti-spam floor:<br/>app has an on-chain entity + bound rule?"}
    Floor -->|no| Reject["400 — below listing floor<br/>(fail-closed)"]
    Floor -->|yes| Write["Write the listing<br/>listed = false"]
    Write --> Deposit["Open listing-fee deposit wallet<br/>return deposit instructions"]
    Deposit -->|"you pay the fee (HST)"| Paid["Deposit confirms"]
    Paid --> Listed["listed = true<br/>visible in the marketplace"]
  1. Anti-spam floor first. Before anything is written, the app must clear a minimum bar — it has to have an on-chain entity with a bound rule. A bare, unconfigured app is rejected with 400 and never reaches the write. The check is fail-closed: even a transient read error blocks the listing rather than admitting a hollow one.
  2. Metadata is written, listed: false. Your developer-settable fields (name, description, category, tags, icon, screenshots, links, targetAudience, and an optional subOffer) land on the listing, but the app is not yet public — listed stays false.
  3. Pay the listing fee to go live. Publishing opens a deposit wallet and returns deposit instructions. When the fee is paid the app flips to listed: true and appears in the public marketplace.

Editorial fields — featured, sortPriority, trustScore — are never developer-settable; publish only seeds them to a neutral default on first publish and never clobbers them on a later metadata edit.

Fees are quoted in HSUITE (HST) and paid on XRPL. The figures below are the defaults; a wallet or dapp reads the live figures from GET /api/v3/baas/marketplace/pricing rather than baking in a copy:

Item Fee Period Notes
Listing 50 HST 30 days Keeps the app publicly listed for the period.
Featured placement 200 HST 7 days A promotional burst, not a standing listing.
Featured slots — — At most 10 apps are featured concurrently; the cap is enforced atomically, so a full board rejects further featured purchases until a slot frees.

The public read surface is served by the Smart Host tier, not the validators, so high-volume browsing never touches signing infrastructure:

Endpoint Returns
GET /api/v3/baas/marketplace/apps Paginated listings — ACTIVE + listed only. Filters: category, targetAudience, featured, bundles.
GET /api/v3/baas/marketplace/apps/:appId A single listing (with a components array when it’s a bundle).
GET /api/v3/baas/marketplace/pricing Current listing + featured fees, periods, and the featured-slot cap.

Every listing carries a trustScore and a featured flag alongside sortPriority; ranking blends them. Trust score is computed editorially — a developer can never set their own.

An owner can compose a listing out of other listed apps by setting includedAppIds (POST .../:appId/bundle). A bundle listing resolves its components to their own public listings for display in one query (never N+1), and the bundles=true filter surfaces only bundles. In this version, a bundle is composition metadata and discovery only — it does not itself grant, mint, or mediate a purchase of any component’s subscription.

A listing can advertise a subOffer (tier, USD price, period); the wallet can show its indicative HST figure at the live HSUITE/USD rate. A browsing wallet checks whether it already holds an active subscription for the app via GET /api/v3/baas/marketplace/apps/:appId/subscription (which reads the wallet off its own signed session — it can never ask about another wallet’s subscription). If the wallet isn’t active, it hands off to the app’s own subscribe flow — the marketplace never resells. Subscription validity is expiry-checked: an expired subscription NFT reports active: false even though it still sits in the wallet (see TokenGate).

  • Report abuse. Anyone can flag a listing (POST .../:appId/report) with a reason. It’s a moderation signal only — it never changes listing state, always returns 202, and deliberately never reveals whether the app exists.
  • Quorum-signed listing record. The platform can produce a quorum-signed listing certificate — the same post-quantum attestation used for trusted deploys — that binds the exact bytes of the public listing. Publishing that record to a public HCS topic, so third parties can rebuild the listing set without trusting the platform, is built, not yet enabled. Today listings live in platform storage.