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.”
The config you write
Section titled “The config you write”Two documents describe a Smart App. You write the deploy manifest; the CLI writes the app config for you when you subscribe.
The deploy manifest
Section titled “The deploy manifest”hsuite deploy and hsuite redeploy read a JSON manifest. This one comes from the Smart App boilerplate:
{ "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. |
The app config
Section titled “The app config”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 app record
Section titled “The app record”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.
Run: how your deployed code boots
Section titled “Run: how your deployed code boots”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.
The two state machines
Section titled “The two state machines”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 an app
Section titled “Retiring an app”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.
Watch: the cluster acts on its own
Section titled “Watch: the cluster acts on its own”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.
Listing your app on the marketplace
Section titled “Listing your app on the marketplace”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.
Only the owner can list
Section titled “Only the owner can list”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.
Publish flow
Section titled “Publish flow”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"]
- 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
400and never reaches the write. The check is fail-closed: even a transient read error blocks the listing rather than admitting a hollow one. - Metadata is written,
listed: false. Your developer-settable fields (name, description, category, tags, icon, screenshots, links,targetAudience, and an optionalsubOffer) land on the listing, but the app is not yet public —listedstaysfalse. - 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: trueand 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.
What a listing costs
Section titled “What a listing costs”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. |
Trust score and discovery
Section titled “Trust score and discovery”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.
Bundles (“puzzle-up”)
Section titled “Bundles (“puzzle-up”)”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.
Subscribing through the marketplace
Section titled “Subscribing through the marketplace”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).
Moderation and the on-chain record
Section titled “Moderation and the on-chain record”- 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 returns202, 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.