Applications
The contents of this page primarily relate to application components. The term "components" in the Operations API and CLI generally refers to applications specifically. See the Components Overview for a full explanation of terminology.
Harper offers several approaches to managing applications that differ between local development and remote Harper instances.
Local Development
dev and run Commands
Added in: v4.2.0
The quickest way to run an application locally is with the dev command inside the application directory:
harper dev .
The dev command watches for file changes and restarts Harper worker threads automatically.
The run command is similar but does not watch for changes. Use run when the main thread needs to be restarted (the dev command does not restart the main thread).
Stop either process with SIGINT (Ctrl+C).
Deploying to a Local Harper Instance
To mimic interaction with a hosted Harper instance locally:
-
Start Harper:
harper -
Deploy the application:
harper deploy \
project=<name> \
package=<path-to-project> \
restart=true- Omit
targetto deploy to the locally running instance. - Setting
package=<path-to-project>creates a symlink so file changes are picked up automatically between restarts. restart=truerestarts worker threads after deploy. Userestart=rollingfor a rolling restart.
- Omit
-
Use
harper restartin another terminal to restart threads at any time. -
Remove an application:
harper drop_component project=<name>
Not all component operations are available via CLI. When in doubt, use the Operations API via direct HTTP requests to the local Harper instance.
Example:
harper deploy \
project=test-application \
package=/Users/dev/test-application \
restart=true
Use
package=$(pwd)if your current directory is the application directory.
Shutdown Cleanup
Applications that start background work — a service, a timer, a connection pool, a buffer that needs flushing — should tear it down when Harper stops or restarts. Restarts are frequent during local development, since harper dev restarts worker threads on every file change, and deploying with restart=true does the same on a running instance.
Harper signals this by calling scope.close() on each worker thread, which emits a 'close' event on the plugin API Scope. Listen for it to run cleanup:
export function handleApplication(scope) {
const service = startService();
scope.once('close', async () => {
await service.close();
});
}
See Cleanup on Shutdown for the full shutdown sequence, including how async cleanup is awaited and the time limit it must finish within.
Remote Management
Managing applications on a remote Harper instance uses the same operations as local management. The recommended approach is to log in first using harper login to store an authentication token:
# Log in once
harper login <remote>
# Provide your username and password when prompted
# Subsequently deploy without credentials
harper deploy \
project=<name> \
package=<package> \
target=<remote> \
restart=true \
replicated=true
Alternatively, credentials can be provided via environment variables (recommended for CI/CD):
export HARPER_CLI_USERNAME=<username>
export HARPER_CLI_PASSWORD=<password>
harper deploy \
project=<name> \
package=<package> \
target=<remote> \
restart=true \
replicated=true
Dedicated Authentication Parameters
Added in: v5.2.0For one-off remote commands, dedicated authentication parameters are also available (not recommended for production):
harper deploy \
project=<name> \
package=<package> \
auth_username=<username> \
auth_password=<password> \
target=<remote> \
restart=true \
replicated=true
Dedicated authentication parameters take precedence over environment variables and saved login tokens. See CLI Authentication for the full order and security guidance.
Package Sources
When deploying remotely, the package field can be any valid npm dependency value:
- Omit
packageto package and deploy the current local directory - npm package:
package="@harperdb/status-check" - GitHub:
package="HarperDB/status-check"orpackage="https://github.com/HarperDB/status-check" - Private repo (SSH):
package="git+ssh://git@github.com:HarperDB/secret-app.git" - Tarball:
package="https://example.com/application.tar.gz"
When using git tags, use the semver directive for reliable versioning:
HarperDB/application-template#semver:v1.0.0
Harper generates a package.json from component configurations and uses a form of npm install to resolve them. This is why specifying a local file path creates a symlink (changes are picked up between restarts without redeploying).
For SSH-based private repos, use the Add SSH Key operation to register keys first.
Deploying by Reference
Added in: v5.2.3Omitting package uploads a snapshot of your working directory. The result is an anonymous artifact: nothing records which commit it came from, so reproducing it later — or stepping back to a previous release — means finding those exact files again.
Deploying by reference sends a pinned git reference instead, and the cluster fetches that exact commit. Redeploying the same reference deploys the same source revision, and rolling back is deploying an older one.
A pinned SHA fixes the source, not the built artifact. The cluster installs and builds from that source on each node, so unpinned dependency ranges, a mutable registry artifact, install scripts, or a different toolchain can still produce different bytes — or a failure — from the same commit. Commit your lockfile if you need the build itself to be reproducible.
harper deploy by_ref=true builds that reference from the local git repository, so you don't assemble the URL yourself:
harper deploy by_ref=true restart=true replicated=true
This resolves the repository's origin remote and the current commit, then deploys package=git+https://github.com/<owner>/<repo>.git#<full commit SHA>.
Parameters:
by_ref- Build the package reference from the local repository.ref(optional) - Deploy a specific commit, tag, or branch instead ofHEAD. Resolved to a commit SHA before it is sent to the cluster. Impliesby_ref.credential(optional) - Set totrueto authenticate the clone with the stored credential for the repository's host. Omit for public repositories.
# Deploy a specific tag
harper deploy ref=v1.2.0 restart=true replicated=true
# Roll back by deploying an older commit
harper deploy ref=9f8c2a1 restart=true replicated=true
A reference is pinned to a SHA, not to the name you typed. Tags and branches are resolved to a full commit SHA before the deploy is sent — from your local checkout when it has the ref, and from the remote when it doesn't (a shallow CI clone usually doesn't). Annotated tags resolve to the commit they point at. This matters on a cluster: peers resolve the package independently, so a tag that moves mid-deploy — or a branch that advances — could otherwise leave nodes running different code.
If a ref can't be resolved either way, the deploy stops rather than sending the name for the cluster to resolve. Run git fetch and retry, or pass a full commit SHA — that needs no resolution and is always accepted.
A ref must also name something a clone can fetch: refs/heads/* and refs/tags/*, or a bare branch or tag name. Anything else — refs/pull/123/head, say — is rejected up front, even if your own checkout can resolve it, because the cluster could resolve that commit and still never check it out.
Commit and push first. The cluster clones from the remote, so it only sees commits that have been pushed. by_ref warns in both directions: when the working tree is dirty (those changes won't be part of the deploy) and when the commit being deployed isn't on any remote branch (the cluster won't be able to clone it). The second check reads your local remote-tracking refs, so run git fetch if you get it for a commit you know you pushed.
The unpushed-commit check is skipped under GitHub Actions, where the runner's checkout is not a branch a git branch -r --contains can see; the dirty-tree warning still applies. On a pull_request run the commit is resolved from the event payload instead, as described below.
In GitHub Actions, by_ref deploys the commit the workflow is running on. On a pull_request run that is the pull request's head commit rather than the merge commit the runner checks out: the merge commit lives under refs/pull/<n>/merge, which a plain clone can't fetch, so the cluster would have no way to resolve it. For a pull request from a fork, the head repository is the fork, and the CLI names it before deploying. If the event payload isn't readable, the deploy stops and asks for the commit explicitly:
harper deploy ref=${{ github.event.pull_request.head.sha }} restart=true replicated=true
Private repositories
Pass credential=true for a private repository. The CLI attaches a credentials reference naming a secret that the cluster resolves in memory at clone time, so no token travels in the operation body or lands on disk:
harper deploy by_ref=true credential=true restart=true replicated=true
The host comes from the package being deployed, so the credential always matches the clone it authenticates. Naming the host explicitly (credential=github.com) still works, but one that doesn't match the package's host is rejected instead of deployed — the clone would never ask for it, and the deploy would fail as though no credential were configured.
Provision that credential once with harper deploy setup=true. See Private-source deploy credentials for how the secret is named and resolved, and add_ssh_key for the SSH-key alternative.
Deploying by reference means the cluster installs and builds the component from source. If your application needs a build step that can't run on the node, keep shipping the built output as a payload deploy instead.
Provisioning a Deploy Credential
Added in: v5.2.3harper deploy setup=true provisions the credential a private deploy needs. It's interactive, and runs once per component and source. It calls get_secrets_public_key, set_secret, and grant_secret, all of which require super_user, so run it with an administrative credential rather than the CI identity it provisions for:
harper deploy setup=true
It asks which private source needs a credential (a GitHub repository or an npm registry), sources a token, and then:
- Fetches the cluster's public key with
get_secrets_public_key. - Encrypts the token locally into an
enc:v1:envelope. - Stores only the ciphertext with
set_secret, in the component-scoped tier. - Grants this component permission to resolve it with
grant_secret. - Prints the
credentialsreference for the deploy to use.
The plaintext never leaves your machine: the operations API, its logs, and replication only ever carry the envelope, and the cluster decrypts it in memory at deploy time. This requires a cluster with secrets custody (Harper Pro / Fabric) — see Client-side encryption.
Prefer a fine-grained PAT. For a GitHub repository the prompt offers, and defaults to, pasting a fine-grained personal access token with Contents: Read-only on that one repository. If you have the gh CLI authenticated it also offers its session token, which is one keypress cheaper but typically carries repo, read:org, gist, and workflow scopes across your whole account; choosing it prints a warning. What this flow seals is durable and replayed on every cold deploy and rollback, so it is worth being the narrowest credential that does the job.
The secret is stored scoped to the component, never in the global processEnv tier that every component and child process can read. If a global secret already exists at the derived name, it is converted to the scoped tier — the name is derived from the component, so a global secret there was never serving anything the scoped one doesn't. Existing grants on the row are preserved.
Because the stored token is durable, later deploys — including re-fetching an older reference — reuse it without re-entering anything.
Dependency Management
Harper uses npm and package.json for dependency management.
During application loading, Harper follows this resolution order to determine how to install dependencies:
- If
node_modulesexists, or ifpackage.jsonis absent — skip installation - Check the application's
harper-config.yamlforinstall: { command, timeout }fields - Derive the package manager from
package.json#devEngines#packageManager - Default to
npm install
The add_component and deploy_component operations support install_command and install_timeout fields for customizing this behavior.
Example harper-config.yaml with Custom Install
myApp:
package: ./my-app
install:
command: yarn install
timeout: 600000 # 10 minutes
allowInstallScripts: true
Example package.json with devEngines
{
"name": "my-app",
"version": "1.0.0",
"devEngines": {
"packageManager": {
"name": "pnpm",
"onFail": "error"
}
}
}
If you plan to use an alternative package manager, ensure it is installed on the host machine. Harper does not support the
"onFail": "download"option and falls back to"onFail": "error"behavior.
Advanced: Direct harper-config.yaml Configuration
Applications can be added to Harper by adding them directly to harper-config.yaml (located in the Harper rootPath, typically ~/hdb).
status-check:
package: '@harperdb/status-check'
The entry name does not need to match a package.json dependency. Harper transforms these entries into a package.json and runs npm install.
Any valid npm dependency specifier works:
myGithubComponent:
package: HarperDB-Add-Ons/package#v2.2.0
myNPMComponent:
package: harper
myTarBall:
package: /Users/harper/cool-component.tar
myLocal:
package: /Users/harper/local
myWebsite:
package: https://harperdb-component
Harper generates a package.json and installs all components into <componentsRoot> (default: ~/hdb/components). A symlink back to <rootPath>/node_modules is created for dependency resolution.
Use
harper get_configurationto find therootPathandcomponentsRootvalues on your instance.
Operations API
Component operations are restricted to super_user roles.
add_component
Creates a new component project in the component root directory using a template.
project(required) — Name of the project to createtemplate(optional) — Git URL of a template repository. Defaults tohttps://github.com/HarperFast/application-templateinstall_command(optional) — Install command. Defaults tonpm installinstall_timeout(optional) — Install timeout in milliseconds. Defaults to300000(5 minutes)install_allow_scripts(optional) — Allow install scripts to run. Defaults tofalse, which causes--ignore-scriptsto be passed to the install command (this is ignored withinstall_command).replicated(optional) — Replicate to all cluster nodes
{
"operation": "add_component",
"project": "my-component"
}
deploy_component
Deploys a component using a package reference or a base64-encoded .tar payload.
project(required) — Name of the projectpackage(optional) — Any valid npm reference (GitHub, npm, tarball, local path, URL)payload(optional) — Base64-encoded.tarfile contentforce(optional) — Allow deploying over protected core components. Defaults tofalserestart(optional) —truefor immediate restart,'rolling'for sequential cluster restartreplicated(optional) — Replicate to all cluster nodesinstall_command(optional) — Install command overrideinstall_timeout(optional) — Install timeout override in millisecondsinstall_allow_scripts(optional) — Allow install scripts to run. Defaults tofalse, which causes--ignore-scriptsto be passed to the install command (this is ignored withinstall_command).
{
"operation": "deploy_component",
"project": "my-component",
"package": "HarperDB/application-template#semver:v1.0.0",
"replicated": true,
"restart": "rolling"
}
drop_component
Deletes a component project or a specific file within it.
project(required) — Project namefile(optional) — Path relative to project folder. If omitted, deletes the entire projectreplicated(optional) — Replicate deletion to all cluster nodesrestart(optional) — Restart Harper after dropping
{
"operation": "drop_component",
"project": "my-component"
}
package_component
Packages a project folder as a base64-encoded .tar string.
project(required) — Project nameskip_node_modules(optional) — Excludenode_modulesfrom the package
{
"operation": "package_component",
"project": "my-component",
"skip_node_modules": true
}
get_components
Returns all local component files, folders, and configuration from harper-config.yaml.
{
"operation": "get_components"
}
get_component_file
Returns the contents of a file within a component project.
project(required) — Project namefile(required) — Path relative to project folderencoding(optional) — File encoding. Defaults toutf8
{
"operation": "get_component_file",
"project": "my-component",
"file": "resources.js"
}
set_component_file
Creates or updates a file within a component project.
project(required) — Project namefile(required) — Path relative to project folderpayload(required) — File content to writeencoding(optional) — File encoding. Defaults toutf8replicated(optional) — Replicate update to all cluster nodes
{
"operation": "set_component_file",
"project": "my-component",
"file": "test.js",
"payload": "console.log('hello world')"
}
SSH Key Management
For deploying from private repositories, SSH keys must be registered on the Harper instance.
add_ssh_key
name(required) — Key namekey(required) — Private key contents (must be ed25519; use\nfor line breaks with trailing\n)host(required) — Host alias for SSH config (used inpackageURL)hostname(required) — Actual domain (e.g.,github.com)known_hosts(optional) — Public SSH keys of the host. Auto-retrieved forgithub.comreplicated(optional) — Replicate to all cluster nodes
{
"operation": "add_ssh_key",
"name": "my-key",
"key": "-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----\n",
"host": "my-key.github.com",
"hostname": "github.com"
}
After adding a key, use the configured host in deploy package URLs:
"package": "git+ssh://git@my-key.github.com:my-org/my-repo.git#semver:v1.0.0"
Additional SSH key operations: update_ssh_key, delete_ssh_key, list_ssh_keys, set_ssh_known_hosts, get_ssh_known_hosts.