Skip to content

Build Your First Smart App

The boilerplate is a working Smart App you copy and make your own. It has two apps:

  • smart-app/: a NestJS backend. It reaches the cluster only through @hsuite/smart-engines-sdk.
  • ionic-dapp/: an Angular/Ionic frontend with wallet connect, served on port 8105.

The backend runs in two modes. With no credentials it runs in open mode on in-memory stores, so everything works on your machine. With testnet credentials the same code runs in cluster mode against the validators.


  • Node.js 24. The boilerplate’s CI and container use it. The CLI supports Node ^22.23.4, ^24.22.1 or >=26.12.1.
  • npm. The boilerplate uses npm, not yarn.
  • Docker with buildx. Only hsuite deploy and hsuite redeploy need it.
  • Access to the boilerplate. See step 1.

  1. The boilerplate repository is private for now. Access comes with developer access to the testnet, after the security audit. Clone it, or copy it into your own repository, then install both apps:

    Terminal window
    # Backend
    cd smart-app
    npm install --legacy-peer-deps
    # Frontend
    cd ../ionic-dapp
    npm ci --legacy-peer-deps

    Use npm ci in ionic-dapp/: its lockfile keeps the @hsuite/native-connect-* packages at matching versions.

  2. Terminal window
    npm install -g @hsuite/smart-engines-cli@latest
    hsuite --version

    Run every hsuite command from smart-app/. The --env and --manifest paths, and the manifest’s build context, resolve against the current directory.

  3. Terminal window
    # Terminal 1
    cd smart-app
    npm run dev # http://localhost:3005, Swagger at /api/docs
    # Terminal 2
    cd ionic-dapp
    npm start # http://localhost:8105

    With no .env.local the backend logs OPEN MODE: no credentials configured … once and keeps every plane in memory. Check it:

    Terminal window
    curl localhost:3005/baas/status
    # {"mode":"memory","appId":null}

    The backend’s other scripts: npm test (offline unit tests), npm run lint, npm run build and npm run build:prod. npm run test:e2e needs a live cluster and credentials.

    • Name it. Set name in smart-app/smart-app.manifest.json. When you subscribe, --app-name sets the name the network records, and defaults to APP_NAME from your env file.
    • Pick the services you use. The manifest’s services lists them, from auth, database, storage, functions and messaging.
    • Write your code. Backend logic goes in smart-app/src/, the interface in ionic-dapp/src/. Keep the backend’s cluster calls on the SDK: the deploy checks that the project is built on it.
    • Size it. backend.replicas and backend.limits (cpu, memory) in the manifest set how much it runs with.
  4. Terminal window
    cd smart-app
    hsuite init

    hsuite init generates a new XRPL testnet wallet, funds it with testnet XRP from the public XRPL testnet faucet, and writes .env.local:

    • the wallet: XRPL_NETWORK, XRPL_ADDRESS, XRPL_PUBLIC_KEY, XRPL_SEED;
    • the cluster: HSUITE_NETWORK, VALIDATOR_URL, SMART_HOST_URL (both https://gateway.testnet.hsuite.network);
    • placeholder app values (APP_ID, APP_NAME, APP_DESCRIPTION) and server settings.

    Then edit .env.local: set PORT=3005. init writes PORT=3000, and the dapp expects the backend on 3005. If the dapp’s calls are refused with a CORS error, add http://localhost:8105 to CORS_ORIGINS.

    Useful flags: --out <file> (default .env.local), --no-fund, and --force. init refuses to overwrite a file that already holds a real XRPL_SEED unless you pass --force.

  5. Cluster mode needs four values in .env.local: SMART_HOST_URL, XRPL_ADDRESS, XRPL_SEED and SUBSCRIPTION_APP_ID. init wrote the first three. The fourth comes from a subscription, and a subscription is paid in testnet HST, which init does not give you.

    Terminal window
    hsuite faucet # testnet HST to your wallet
    hsuite subscribe --app-name "My App" # creates the app and pays the deposit
    • hsuite faucet pays testnet HST to the wallet in .env.local. If the wallet has no HST trustline yet, it sets one first. It prints the amount, the transaction hash and what remains. When you are rate-limited it says so and exits with code 1.
    • hsuite subscribe signs in with your wallet, creates the app, pays the HST deposit, then mints the subscription NFT and accepts it into your wallet. Creating the app can take a few minutes, so let it run. It writes SUBSCRIPTION_APP_ID and APP_ID as soon as the app exists, and SUBSCRIPTION_NFT_SERIAL once the NFT is accepted. The tier defaults to builder (--tier builder|growth|scale|enterprise). If it stops part-way, run it again: it resumes the saved app.

    Restart the backend after any change to .env.local, then check it:

    Terminal window
    npm run dev
    curl localhost:3005/api/health
    # … "baas":{"registered":true,"appId":"<your appId>", … }

    The boot log shows BaaS auth step 1/3 … 3/3, then Bound to existing smart-app <appId>. If only some of the four values are set, the backend is not in open mode either: every non-public route answers 503 and the log names what is missing.

  6. Your business logic goes in smart-app/src/. The boilerplate already calls the SDK’s data planes, and each call acts for the signed-in customer, through client.forCustomer(customerToken). The dapp signs its user in and sends that bearer with every request to your backend.

    The pattern the boilerplate uses
    import type { BaasClient } from '@hsuite/smart-engines-sdk';
    async function saveNote(client: BaasClient, customerToken: string, text: string) {
    const db = client.forCustomer(customerToken).db;
    const { document } = await db.insert('notes', { text });
    const proof = await db.getDocumentProof('notes', document._id);
    return { id: document._id, proof };
    }

    What the boilerplate demonstrates, and where:

    Plane SDK calls Backend routes
    Database db.insert, db.update, db.delete, db.find, db.getDocumentProof /baas/db
    Storage storage.upload, storage.downloadWithMeta, storage.listFiles, storage.delete /baas/storage
    Messaging messaging.publish, messaging.getHistory /baas/messaging
    Functions functions.deploy, list, get, getCode, invoke, eval, getLogs, delete, pause, resume /functions
    Rules rules.publish, rules.listByOwner, rules.get, rules.simulate, with the Rules builders /rules
    Entities entities.createToken, createAccount, createTopic, prepareCreateAccount, prepareCreateToken, get, delete /entities

    /api/health and /baas/status report the app’s state. Configuration is read from .env.local; a key that src/config/env-validation.ts does not declare is dropped at boot, so add your own keys there first.

  7. The deploy does not compile your backend: the image copies a prebuilt dist/. Build it with build:prod:

    Terminal window
    cd smart-app
    npm run build:prod

    The deploy aborts if the image contains .map, .d.ts or .tsbuildinfo files, and plain npm run build emits them.

    The frontend needs its production settings. Copy ionic-dapp/src/environments/environment.prod.example.ts to environment.prod.ts in the same folder and replace both REPLACE_BEFORE_DEPLOY:* values:

    • walletConnectProjectId: your WalletConnect project id.
    • smartAppUrl: the public URL of your deployed backend.

    While a placeholder remains, the deploy stops with frontend still contains REPLACE_BEFORE_DEPLOY placeholders.

  8. The deploy is described by smart-app/smart-app.manifest.json. The boilerplate’s manifest:

    smart-app/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",
    "API_KEY_ENABLED": "false",
    "RATE_LIMIT_TTL": "60",
    "RATE_LIMIT_MAX": "120",
    "REQUEST_TIMEOUT": "30000"
    }
    },
    "frontend": {
    "build": "cd ../../ionic-dapp && npm run build -- --output-path ../smart-app/public",
    "dir": "public"
    }
    }
    • Required: name, services, backend.port and backend.tag. backend.port is the port your backend listens on inside the container, and 8080 is reserved.
    • Tags are immutable. Every new version, and every retry, needs a fresh backend.tag.
    • Outbound traffic is blocked by default. A deployed app reaches DNS and the platform’s own endpoint only. To call an external API, request it in backend.egress, for example [{ "host": "api.example.com", "port": 443 }] (bare hostnames, at most 50 entries). The network may refuse a request.

    The container’s environment is your --env file plus backend.env. Every non-empty key in the env file is forwarded, encrypted, except the platform URLs (SMART_HOST_URL, VALIDATOR_URL, GATEWAY_URL) and local-only toggles. A non-empty value in the env file wins over backend.env. Your .env.local holds PORT=3005 and NODE_ENV=development, so deploy from a copy with the production values:

    Terminal window
    cd smart-app
    cp .env.local .env.deploy
    # in .env.deploy: set PORT=3200 (the manifest's backend.port) and NODE_ENV=production
    hsuite deploy --manifest smart-app.manifest.json --env .env.deploy

    The deploy needs a subscription: without SUBSCRIPTION_APP_ID it stops at once and tells you to run hsuite subscribe. It also refuses an env file without XRPL_SEED. Then it:

    1. checks that the project is built on @hsuite/smart-engines-sdk;
    2. signs in with your wallet;
    3. runs the frontend hook, checks the bundle and uploads it;
    4. builds and pushes the image;
    5. signs a build attestation with your wallet;
    6. deploys, and waits until the app is running.

    It prints ✓ deployed: <url> and writes DEPLOYED_APP_ID to the env file. See Deployment for what the platform checks on the way.

  9. Terminal window
    hsuite status --env .env.deploy --watch
    hsuite usage --env .env.deploy
    hsuite list --env .env.deploy
    • hsuite status prints the app’s lifecycle state (PENDING_SUBSCRIPTION, ACTIVE, SUSPENDED or RETIRED) and, once deployed, its URL, runtime state, replicas, image, last error, declared egress and history. It ends with a check of every cluster. --watch polls until the runtime is RUNNING or FAILED; --json prints the raw status.
    • hsuite usage prints today’s usage by category, then the usage percentage, any overage, and whether the app is blocked or in a grace period.
    • hsuite list lists your deployed apps.

    hsuite topup refills the subscription’s prepaid credit without changing the tier.

  10. hsuite redeploy updates the same app with a new image tag, a new frontend bundle, or both. Rebuild first, then pass a fresh tag:

    Terminal window
    npm run build:prod
    hsuite redeploy --manifest smart-app.manifest.json --env .env.deploy --tag v2

    It targets DEPLOYED_APP_ID from the env file (or --app-id <id>) and prints ✓ redeployed: <url>. If the URL hsuite deploy printed differs from the smartAppUrl you set, update environment.prod.ts and redeploy.