@hsuite/smart-engines-cli
v1.5.1
Published
HSuite CLI for smart-app deployment and management
Maintainers
Readme
@hsuite/smart-engines-cli
hsuite — the developer bootstrap CLI for Smart Engines V3.
Takes you from "empty directory" to "smart-app running against the live testnet
cluster" in under a minute, then manages the deployment's lifecycle.
npm install -g @hsuite/smart-engines-clihsuite init # XRPL wallet + faucet + .env.local
hsuite subscribe --env <file> # paid smart-app subscription on cluster (tier builder|growth|scale|enterprise)
# deployment lifecycle (manifest-driven):
hsuite deploy --manifest <f> # THE canonical path — fresh deploy AND redeploy
hsuite redeploy --manifest <f> # thin convenience: distinct --app-id / --tag override
hsuite update --replicas 3 # patch runtime config (cpu/memory/degree/replicas/env)
hsuite suspend | resume | delete
hsuite status [--watch] | listXRPL-only. The CLI generates an XRPL keypair, funds it via the testnet faucet, and uses it for Web3 challenge-response auth against the cluster. No Hedera credentials needed — the cluster's validator/host pods pay their own HCS fees.
hsuite init
Generate a fresh XRPL testnet wallet, fund it (~1000 testnet XRP via the
official Ripple faucet), and write an .env.local with everything the
showcase smart-app expects.
hsuite init # writes ./.env.local (testnet)
hsuite init --out smart-app/.env.local # custom output path
hsuite init --network devnet # devnet faucet instead
hsuite init --no-fund # generate wallet only, skip faucet
hsuite init --force # overwrite an existing .env.local
hsuite init --gateway https://... # override the cluster gateway URLThe generated .env.local:
APP_ID=smart-app-showcase-local
APP_NAME=Smart App Showcase (local)
VALIDATOR_URL=https://v3-testnet-gateway.hsuite.network
SMART_HOST_URL=https://v3-testnet-gateway.hsuite.network
XRPL_NETWORK=testnet
XRPL_ADDRESS=r...
XRPL_PUBLIC_KEY=...
XRPL_SEED=s... # ⚠️ DO NOT COMMIT
# NestJS dev config
NODE_ENV=development
PORT=3000
API_KEY_ENABLED=false
CORS_ORIGINS=http://localhost:8101,http://localhost:8100,http://localhost:4200
...
# Subscription — run `hsuite subscribe` to register a paid smart-app. Default tier is builder.
SUBSCRIPTION_TIER=builder
# SUBSCRIPTION_APP_ID= # populated by `hsuite subscribe`hsuite subscribe
Register a paid smart-app subscription on the cluster. Every connection is
paid: pick a tier (builder | growth | scale | enterprise, default builder),
the cluster spins up a TSS deposit wallet and returns deposit instructions, and
the subscription NFT mints once the HSUITE deposit confirms on-chain. DKG runs
so the cluster-issued appId becomes your smart-app's on-chain identity.
hsuite subscribe --env .env.local
hsuite subscribe --env .env.local --tier growth
hsuite subscribe --env .env.local --name "My Smart App"
hsuite subscribe --env .env.local --services database,storage,messaging
hsuite subscribe --env .env.local --gateway https://my-cluster.example.comFlow:
- Reads
XRPL_*+APP_NAMEfrom.env.local - Builds a signer using
ripple-keypairs.sign - Authenticates via
BaasClient.authenticate({chain: 'xrpl', ...})against the cluster - Calls
BaasClient.deployment.init({name, port, services})— the new four-step deploy flow's step 1 (legacyregister()removed in PR-A); the cluster runs the per-entity DKG ceremony and returns the DKG entityId asappId - Appends
SUBSCRIPTION_APP_ID=<appId>andAPP_ID=<appId>to your.env.local
After this, your smart-app boots in cluster-mode automatically.
Deployment commands
Eight commands take a smart-app from a manifest to a running, manageable deployment on the cluster. They all share base flags:
| Flag | Default | Meaning |
|---|---|---|
| --env <file> | .env.local | Env file with the XRPL auth creds |
| --gateway <url> | https://gateway.testnet.hsuite.network | Public gateway/host URL |
| --app-id <id> | DEPLOYED_APP_ID → SUBSCRIPTION_APP_ID | App to act on |
Every command constructs exactly one BaasClient
({ hostUrl, appId, pathPrefix: '/host', allowInsecure, timeout }) and
authenticates it with the env XRPL signer — connecting straight to the public
round-robin gateway (no cluster-discovery factory, whose per-cluster hostnames
aren't reachable from a dev machine).
The manifest (one canonical shape)
Source of truth: packages/hsuite-cli/src/_lib/deploy-manifest.ts.
Every smart-app is a NestJS backend, optionally fronted by an ionic SPA.
The manifest has ONE shape: a required backend block plus an optional
frontend block.
{
"name": "my-smart-app",
"services": ["database", "storage", "messaging", "functions"], // ('auth'|'database'|'storage'|'functions'|'messaging')[]
"degree": 3, // clusters to span (>=3; 3 is the floor)
"backend": { // REQUIRED — the NestJS image
"port": 3200, // backend listen port, 1..65535, NEVER 8080 (reserved for the edge sidecar)
"tag": "v1", // image tag (REQUIRED)
"replicas": 1, // pods per cluster (default 1)
"context": ".", // docker build context (default '.')
"dockerfile": "Dockerfile", // default 'Dockerfile'
"platform": "linux/amd64", // default 'linux/amd64' (cluster-node arch)
"build": "yarn build", // optional pre-build shell cmd, run in context
"env": { "NODE_ENV": "production" }, // NON-SECRET overrides only — merged over the auto-forwarded .env.local runtime env (manifest wins)
"limits": { "cpu": "500m", "memory": "512Mi" }
},
"frontend": { // OPTIONAL — ionic SPA, served by the EDGE sidecar (NOT the backend)
"build": "ng build", // optional pre-build command
"dir": "www", // dir to tar into the SPA bundle
"bundlePath": null // OR a pre-built tarball (wins over `dir`)
}
}Runtime env: auto-forwarded from .env.local (do NOT hand-copy secrets)
hsuite deploy automatically forwards your .env.local runtime creds into the
deployed container — so the deployed app runs with the same identity + config
it runs with locally. The deployed smart-app authenticates as your own XRPL
wallet (the XRPL_SEED in .env.local, whose address holds the subscription
NFT); there is no separate app identity. You therefore never hand-copy
XRPL_SEED / HEDERA_PRIVATE_KEY / chain creds into a committed manifest — a
manual step that, if forgotten, silently shipped a credential-less, open-mode
app (/baas/status → {"mode":"memory","appId":null}; BaaS routes 503). See
issue #1564.
What gets forwarded: the runtime-relevant keys from your .env.local — the
same set the smart-app's env schema reads (XRPL_*, SUBSCRIPTION_APP_ID,
APP_*, VALIDATOR_URL/SMART_HOST_URL, AI/Discord config, …). Purely
deploy-time / CLI-only knobs (e.g. SUBSCRIPTION_NFT_SERIAL, VALIDATOR_API_KEY)
are not forwarded. The list lives in
packages/hsuite-cli/src/_lib/runtime-env.ts (RUNTIME_ENV_KEYS), mirroring the
smart-app schema (apps/showcase/smart-app/src/config/env-validation.ts in the
hsuite-ecosystem repo).
Precedence: the forwarded .env.local values are the base;
manifest.backend.env is overlaid last and WINS for any key it sets. Use
backend.env for explicit non-secret per-deploy overrides (ports, feature
flags, Discord/AI endpoints). Secrets flow automatically from .env.local and do
not belong in a committed manifest. Everything lands in the app's PQC-encrypted
(kyber-aes-v1) per-app customer-env Secret — the pod's runtime env, envFrom-
mounted on the backend container. Encryption at rest is the containment boundary;
no secret is ever written into a committed file.
Fail-loud: if a deploy would ship the app credential-less (no XRPL_SEED
reaching the container) the CLI errors rather than silently booting open mode.
An intentional open-mode deploy stays valid via the explicit --allow-open-mode
flag, which deliberately withholds the seed from the container (open mode)
while still using it locally to sign the required build attestation.
The same forwarding + fail-loud applies to
hsuite redeploy.
Backend-only example (no frontend block — the edge proxies all paths to the
backend):
{
"name": "my-api",
"services": ["database"],
"degree": 3,
"backend": { "port": 3200, "tag": "v1" }
}Relative
frontend.dir/frontend.bundlePath(and the frontendbuildhook) resolve against the backend's buildcontext(the smart-app root) — NOT the directory you ranhsuitefrom. A bare"public"means<smart-app>/public.degreeis validated client-side against the host floor (>= 3) so a bad value fails with a clear CLI error instead of a server 500.
Serving model: the public-vs-edge split
Every deployed customer pod has two containers:
- The customer backend listens on loopback at
backend.port(e.g.3200). It is NOT exposed at the pod boundary, and8080is forbidden for the backend. - A platform edge sidecar (Caddy) listens on port
8080— the pod's exposed/Service port. The edge reverse-proxies API requests to the backend on loopback and serves the SPA.
The ionic SPA is served by the edge sidecar, not the backend. The optional
frontend block is tarred and uploaded (uploadFrontend) into the cluster BaaS
storage tier, content-addressed by sha256. At pod start a bundle-fetcher init
container fetches that bundle into a shared emptyDir, which the edge serves
read-only from /srv/spa, falling back to the SPA shell (index.html) for
client-side routing. For a backend-only app (no frontend block) the edge
proxies all paths to the backend.
The SPA is also commonly baked into the backend image's own
public/— but in the deployed pod it is the edge-served uploaded bundle that the public sees on port8080.(Refs:
apps/smart-deployer/src/reconciler/caddyfile-renderer.service.ts,apps/smart-deployer/src/reconciler/k8s-resource-builder.service.ts.)
Multi-cluster image distribution
harbor.testnet.hsuite.network is public-DNS round-robin over the 3 cluster
ingress IPs, each fronting its own Harbor. A single docker push lands the image
on only one cluster. hsuite deploy therefore fans the image out to every
cluster Harbor itself, synchronously, BEFORE reconciling: it resolves the
registry host's A-records, copies the pushed image to each cluster at the
Registry-v2 level (TLS-SNI pinned to the federated hostname, 401/Bearer token
auth, blob copy + manifest PUT), then re-verifies the exact pushed digest is
served by every cluster and ABORTS the deploy if any cluster can't be made to
serve it. This replaces reliance on the (unreliable) Harbor cross-cluster
replication mesh for customer images — the mesh is no longer load-bearing. The
push-robot credential is BLS-deterministic and identical on all 3 cluster
Harbors, which is what lets one credential authenticate the fan-out against every
cluster. --skip-distribute opts out (single-cluster / dev registry).
(Implementation: packages/hsuite-cli/src/_lib/registry-fanout.ts.)
Deploy strategy (Recreate vs RollingUpdate)
The strategy is driven by replica count
(apps/smart-deployer/src/reconciler/k8s-resource-builder.service.ts
buildDeployment):
- Single-replica apps (
replicas <= 1) useRecreate. A single-replica tenant CANNOT surge — the per-tenantResourceQuotaonly fits one pod — soRollingUpdatewould deadlock. Pods are stateless (no RWO PVC), so Recreate-in-place is safe. - Multi-replica apps use zero-downtime
RollingUpdate{maxSurge:1, maxUnavailable:0}.
The strategy field on the manifest/deploy request is advisory: a
recreate request is coerced to RollingUpdate for multi-replica apps (with a
warning).
hsuite deploy — THE canonical deploy path
hsuite deploy --manifest <file> is the single, canonical path to deploy a
customer smart-app (NestJS backend + optional ionic SPA) across the 3-cluster
testnet. It handles BOTH a fresh deploy AND a redeploy of an existing/active
app. There is no hand-rolled deploy-*.cjs script and no manual per-cluster
docker push — exactly one path: hsuite deploy.
Mint-first / deploy-last: the developer's SUBSCRIPTION_APP_ID IS the deploy
target (created + paid by hsuite subscribe; SUBSCRIPTION_APP_ID ==
DEPLOYED_APP_ID == DKG entityId). deploy allocates nothing — it
authenticates as that id and fetches push creds for it (pushCredentials,
idempotent).
Flow: resolve SUBSCRIPTION_APP_ID → connectAndAuth → pushCredentials →
docker build+push (backend) → fan the image out to every cluster Harbor
(verify + abort on any miss) → tar + uploadFrontend (frontend) → sign a build
attestation → deploy(). Writes DEPLOYED_APP_ID to the env file.
hsuite deploy --manifest ./my-app.json --env .env.local
hsuite deploy --manifest ./my-app.json --strategy recreate
hsuite deploy --manifest ./my-app.json --docker-host unix:///path/docker.sock
hsuite deploy --manifest ./my-app.json --skip-build --skip-push # image already pushed
hsuite deploy --manifest ./my-app.json --skip-distribute # single-cluster / dev registry
hsuite deploy --manifest ./my-app.json --subscription-app-id sub_… # override the deploy targetFlags: --manifest <file>, --env <file> (default .env.local), `--project
The docker login uses the init-issued one-time robot creds via
execFileSync('docker', ['login', server, '-u', username, '--password-stdin'])
— the robot username (robot$project+name) is a discrete argv element and the
password is fed on stdin, so the $ is never shell-expanded.
hsuite redeploy — thin convenience
redeploy is NOT a separate required path — hsuite deploy already redeploys an
existing/active app. redeploy is a thin convenience for passing a distinct
deployed --app-id and/or a --tag override. It rebuilds + pushes the
backend image, fans it out to every cluster (same --skip-distribute opt-out),
re-uploads the SPA bundle when the manifest carries a frontend block, then
deploy()s.
hsuite redeploy --manifest ./my-app.json --tag v2
hsuite redeploy --manifest ./my-app.json --app-id app_… --strategy recreate
hsuite redeploy --manifest ./my-app.json --skip-distributehsuite update
Patch runtime config without a new image — at least one field required.
hsuite update --cpu 500m --memory 512Mi
hsuite update --replicas 3 --degree 3
hsuite update --strategy recreate
hsuite update --env-set NODE_ENV=production --env-set LOG_LEVEL=infohsuite suspend / resume / delete
hsuite suspend # scale the runtime to zero
hsuite resume # scale it back up
hsuite delete # tear down (confirms; --yes to skip, required when non-TTY)
hsuite delete --yeshsuite status / list
hsuite status # state + runtime (runtimeState / replicas / lastError)
hsuite status --watch # poll until RUNNING / FAILED
hsuite status --json
hsuite list # table of {appId, name, status}
hsuite list --jsonThe deployed appId is appended to .env.local as DEPLOYED_APP_ID.
End-to-end (1-minute bootstrap)
mkdir my-smart-app && cd my-smart-app
npm install -g @hsuite/smart-engines-cli
hsuite init # → .env.local with funded XRPL wallet
hsuite subscribe --env .env.local # → paid (builder) subscription appId on cluster
cat .env.local | grep XRPL_ADDRESS # → your wallet address
cat .env.local | grep SUBSCRIPTION_APP_ID # → your smart-app's DKG entity IDThat's it — you now have:
- A funded XRPL testnet wallet
- A registered smart-app on the live Smart Engines V3 testnet cluster
- An
.env.localyour NestJS backend can consume directly
What the CLI does NOT do
- Pay HCS fees. The cluster's validator/host pods do that from their own pod-local Hedera operator accounts. The smart-app never sees Hedera credentials.
- Confirm the HSUITE deposit for you.
subscribecreates the subscription and returns deposit instructions, but you fund the TSS deposit wallet yourself; the subscription NFT mints (and the smart-app goes ACTIVE) only once that deposit confirms on-chain. - Configure your smart-app's business logic. If your smart-app does Hedera-side or Solana-side or any other-chain ops as part of its app logic, that's separate from the dev wallet and you configure it in your app's own code/env.
Use programmatically
The hsuite package is a CLI only — its entry point (dist/index.js) is an executable that parses process.argv on load and exposes no importable API. There are no initWallet / subscribeFreeTestnet / deploySmartApp functions to import.
For programmatic access from your own code, use the SDK directly — see Companion packages:
import { /* ... */ } from '@hsuite/smart-engines-sdk';To automate the CLI itself, shell out to the hsuite binary from your build scripts or CI:
hsuite init --out .env.local
hsuite subscribe --env .env.local --tier builderCompanion packages
@hsuite/smart-engines-sdk— the underlying SDK thathsuitewraps. Use it directly when you need finer-grained control over auth, BaaS calls, or chain operations.- hsuite-ecosystem/apps/showcase — a reference NestJS backend + Ionic dApp that demonstrate the full integration. Fork it as a starting point.
Peer dependencies
| Package | Why |
|---|---|
| @hsuite/smart-engines-sdk | Auth + BaaS API surface |
| xrpl | XRPL keypair generation + signing |
| commander, chalk, ora, dotenv, uuid | CLI ergonomics (UI + env parsing) |
All bundled — npm install -g @hsuite/smart-engines-cli pulls everything.
License
MIT.
