DevelopersTypeScript SDK

Hostwolf Apps SDK for JavaScript & TypeScript

@hostwolf/apps-sdk is the official typed client for the Hostwolf Apps API. With it you can deploy and run your apps from code: projects, applications, deployments and logs, environment variables, managed databases, autoscaling, volumes and more.

npm install https://app.hostwolf.net/dl/hostwolf-apps-sdk-0.2.1.tgz

The SDK is served from app.hostwolf.net rather than the npm registry. The command above adds it to your package.json as "@hostwolf/apps-sdk": "https://app.hostwolf.net/dl/hostwolf-apps-sdk-0.2.1.tgz", and you import it as @hostwolf/apps-sdk. pnpm, yarn and bun accept the same URL. Every release has its own URL; see Releases.

It works in Node 18+, Bun and Deno, and has no runtime dependencies.

Quick start#

Create an API token in the console under Keys & Tokens → API tokens, then:

import { HostwolfApps } from '@hostwolf/apps-sdk';

const hw = new HostwolfApps({ token: process.env.HOSTWOLF_APPS_TOKEN! });

const server = (await hw.servers.purchased()).find((s) => s.kind === 'app' && s.status === 'ready')!;
const { uuid: project } = await hw.projects.create({ name: 'shop' });

const app = await hw.applications.createFromGit({
    project_uuid: project,
    environment_name: 'production',
    server_uuid: server.server_uuid!,
    git_repository: 'https://github.com/acme/shop',
    git_branch: 'main',
    build_pack: 'nixpacks',
    ports_exposes: '3000',
});
await hw.applications.setEnvs(app.uuid, { NODE_ENV: 'production' });

const deployment = await hw.deployAndWait(app.uuid);
console.log(deployment.status, await hw.applications.logs(app.uuid, { lines: 20 }));

Resources are addressed by their uuid. Response objects use the API's own field names (snake_case), and their TypeScript types are exported, e.g. Application, Deployment and ManagedDatabase.

Options#

new HostwolfApps({
    token: '12|abc...',                              // required
    baseUrl: 'https://console.app.hostwolf.net',     // default
    timeoutMs: 30_000,                               // per request
    maxRetries: 2,                                   // 429s, and 502/503/504 on GET
    maxRetryWaitMs: 10_000,                          // longer Retry-After waits fail fast instead
    language: 'de',                                  // error messages in en, de, fr, es or fi
    fetch: customFetch,                              // optional
});

Projects and environments#

MethodWhat it does
projects.list() / get(uuid)Projects; get includes the environments
projects.create({ name })New project with a production environment
projects.update / deleteRename, or delete an empty project
projects.environments(uuid)Environments of a project
projects.environment(uuid, 'staging')One environment (by name or uuid) with its resources
projects.createEnvironment / updateEnvironment / deleteEnvironmentManage environments
projects.sharedEnvs / environmentSharedEnvsVariables shared by every app of a project or environment

Applications#

MethodWhat it does
applications.list({ tag })Every application of the team
applications.get(uuid)One application with its settings
applications.createFromGit(input)From a public repository (nixpacks, railpack, dockerfile or static)
applications.createFromPrivateRepo(input)From a private repository with a deploy key (private_key_uuid)
applications.createFromGithubApp(input)From a repository of a connected GitHub App (github_app_uuid); push deploys work out of the box
applications.createFromDockerfile(input)From Dockerfile text, no repository needed
applications.createFromImage(input)From a prebuilt image such as ghcr.io/acme/web
applications.update(uuid, changes)Name, domains, ports, branch, build and start commands, health check, limits
applications.deploy(uuid, { force })Build and start; returns the deployment_uuid
applications.restart / stopRestart without a rebuild, or stop
applications.logs(uuid, { lines })Recent container output (token needs read:sensitive)
applications.metrics(uuid, { minutes })CPU and memory history
applications.rollbackImages / rollback(uuid, tag)Redeploy an earlier image
applications.delete(uuid, { deleteVolumes })Delete the app (and by default its volumes)

Apps run on your app servers. server_uuid is the server_uuid field of servers.purchased() (or the uuid from servers.list()). Leave domains out to get a generated *.app.hostwolf.net address.

Environment variables#

MethodWhat it does
applications.envs(uuid)Variables; values are null unless the token has read:sensitive
applications.setEnvs(uuid, { KEY: 'value' })Create or update several at once
applications.setEnv(uuid, { key, value, is_shown_once })Create or update one
applications.createEnv / updateEnvStrict create (409 if it exists) and update
applications.deleteEnv(uuid, envUuid) / deleteEnvByKey(uuid, key)Remove a variable
teams.sharedEnvs() and friendsTeam-wide variables, used as {{team.KEY}}

Running containers see changed variables after the next deploy.

Deployments#

const { deployment_uuid } = await hw.applications.deploy(app.uuid);
const done = await hw.deployments.wait(deployment_uuid!, {
    timeoutMs: 10 * 60_000,
    onStatus: (status) => console.log(status), // queued, in_progress, finished
});

wait and deployAndWait throw a HostwolfAppsError with code deployment_failed (and the last log lines in the message), deployment_cancelled or timeout. Pass throwOnFailure: false to get the failed deployment back instead.

MethodWhat it does
deployments.running()Deployments queued or running now, across the team
deployments.get(uuid)One deployment, with its build log in logs
parseDeploymentLogs(deployment)The build log as lines ({ output, type, timestamp })
deployments.forApplication(uuid, { skip, take })A page of an app's deployments, newest first
deployments.allForApplication(uuid)Async iterator over all of them
deployments.deploy({ uuid, tag, force })Deploy several apps by uuid or tag
deployments.cancel(uuid)Cancel a queued or running deployment

Servers#

MethodWhat it does
servers.purchased() / purchasedServer(uuid)App servers and VPS you bought: size, city, status, price and server_uuid
servers.list() / get(uuid)App servers apps can run on
servers.resources(uuid) / domains(uuid)What runs on a server, and its domains
servers.metrics(uuid, { minutes })CPU and memory history
servers.destinations(uuid)A server's Docker networks (for multi-server)
servers.traffic(uuid, { month })Outbound traffic of a purchased server and its bill so far (1 TB a month included)
servers.teamTraffic({ month })The same for every server of the team, with the total
servers.regions({ size })Cities with the monthly price per size, and where autoscaling may add servers

Buying and cancelling servers is only possible in the console, so an API token can never buy a server.

Multi-server#

Run copies of one app on several of your app servers. Hostwolf builds the image once, starts a copy on every server and load-balances between them.

await hw.applications.addServer(app.uuid, otherServer.server_uuid!);
await hw.deployAndWait(app.uuid);
console.log(await hw.applications.destinations(app.uuid)); // primary + copies

removeDestination(appUuid, destinationUuid) stops the copy on that server again.

Managed databases#

PostgreSQL, MySQL/MariaDB and Redis on Hostwolf database servers, separate from your app servers, with TLS, nightly off-site backups and point-in-time recovery for PostgreSQL.

const plans = await hw.databases.catalog();             // engines, plans, pool sizes, regions, prices
const db = await hw.databases.create({ engine: 'postgres', plan: 'small', region: 'vultr:ams' });
await hw.databases.waitUntilReady(db.uuid);
await hw.databases.attach(db.uuid, app.uuid);           // DATABASE_URL (pooled) + DATABASE_DIRECT_URL
await hw.deployAndWait(app.uuid);

A database is billed hourly on your server subscription from the moment it is created. create sends an Idempotency-Key, so a retried call never makes a second database; pass your own with { idempotencyKey }.

connection(uuid) returns two URLs. url goes through the connection pooler (PgBouncer on 6432, ProxySQL on 6033): use it in your app. direct_url reaches the database itself: use it for migrations, LISTEN/NOTIFY, session settings and long transactions. Redis has one URL.

MethodWhat it does
databases.list() / get(uuid)Your databases with engine, plan, region, status, price, limits, upgrades and advice
databases.connection(uuid)The pooled and direct URLs with the password (token needs read:sensitive)
databases.attach(uuid, appUuid, { envKey, overwrite })Save the pooled URL as DATABASE_URL and the direct one as DATABASE_DIRECT_URL (Redis: REDIS_URL). If the app already has them with another value it throws conflict with error.conflicts and error.suggestedEnvKey; pass envKey or overwrite: true
databases.upgrade(uuid, plan, { confirm: true })Move to a bigger plan from upgrades; the URL stays the same
databases.setEvictionPolicy(uuid, policy)Redis: allkeys-lru for a cache, noeviction for queues (applied live)
databases.backup(uuid)Start an extra backup now
databases.delete(uuid)Delete it and stop billing; support can restore it for 7 days

upgrade without { confirm: true } changes nothing: it throws a HostwolfAppsError with code confirm_required, and error.quote holds the new monthly price to show first.

Autoscaling#

const { policy, blocker, status } = await hw.autoscaling.get(app.uuid);
await hw.autoscaling.enable(app.uuid, { min_copies: 1, max_copies: 4, monthly_cap_usd: 40 });

Autoscaling adds app servers near your visitors when CPU stays high, and removes them when it drops. Added servers are billed by the hour at the regular price, up to monthly_cap_usd. blocker says why it can't be turned on yet (for example, no active server subscription). Fields you leave out keep their value; disable(uuid) turns it off. cities lists the region ids allowed_cities accepts for the app, with the hourly price there; servers.regions() lists every city.

Storage volumes#

Block storage for your app servers and VPS: 10 to 1000 GB, mounted at /mnt/volume-<id> on the server, billed by the hour. Calls that raise the bill need { confirm: true }; without it they change nothing and throw code confirm_required with the price in error.quote.

const offer = await hw.volumes.offer(server.uuid, { sizeGb: 50 });   // sizes allowed here and the price
const volume = await hw.volumes.create(server.uuid, { size_gb: 50, autogrow_max_gb: 200 }, { confirm: true });
await hw.volumes.waitFor(volume.id);                                  // mounted in about a minute
await hw.volumes.resize(server.uuid, volume.id, 100, { confirm: true });
MethodWhat it does
volumes.list(serverUuid) / listAll({ status }) / get(id)A server's volumes, or every volume of the team (status: 'detached' for detached ones)
volumes.offer(serverUuid, { sizeGb })Whether the server's city sells volumes, the sizes and a price
volumes.create(serverUuid, { size_gb, backups, autogrow_max_gb }, { confirm })Add a volume (backups on by default)
volumes.resize / setBackups / setAutogrow / updateEnlarge (never shrink), turn backups on or off, set the auto-grow limit
volumes.detach(serverUuid, id)Unmount and detach it; it keeps its files and stays billed
volumes.attach(id, serverUuid, { confirm })Attach a detached volume to a server in the same city
volumes.backups(id) / backupNow(serverUuid, id)Backups that can be restored, or one now
volumes.restore(serverUuid, id, backupId, 'same' | 'new', { confirm })Restore into the volume itself or into a new one
volumes.restoreDetached(id, backupId, serverUuid, { confirm })Restore a detached volume's backup into a new volume
volumes.delete(id, { confirm: true })Delete it and its files

When a server is deleted, its volumes are detached and kept for 7 days, free of charge (kept_until); attach one to another server in the same city meanwhile, or it is deleted.

App volumes#

MethodWhat it does
applications.storages(uuid)Persistent volumes and file mounts
applications.createStorage(uuid, { type: 'persistent', name, mount_path })Add a volume (redeploy to mount it)
applications.updateStorage / deleteStorageChange or delete one
applications.setVolumeBackup(uuid, storageUuid, { frequency: 'daily' })Back up a volume on a schedule
applications.runVolumeBackup / deleteVolumeBackupBack up now, or remove the schedule

Teams#

MethodWhat it does
teams.current() / members()The token's team and its members
teams.update({ name })Rename the team
teams.auditEvents({ page }) / allAuditEvents()The team's audit log (admins and owners)

Pagination#

Lists that the API splits into pages come in two forms: one method returns a single page, and an all… method returns an async iterator over every item. collect turns an iterator into an array:

import { collect } from '@hostwolf/apps-sdk';

const deployments = await collect(hw.deployments.allForApplication(app.uuid), 100);
for await (const event of hw.teams.allAuditEvents({ action: 'deploy' })) console.log(event.description);

Errors#

Failed calls throw a HostwolfAppsError.

import { HostwolfAppsError } from '@hostwolf/apps-sdk';
try {
    await hw.applications.createFromDockerfile({ project_uuid, server_uuid, dockerfile });
} catch (e) {
    if (e instanceof HostwolfAppsError && e.isValidation) console.log(e.errors); // { field: ['message'] }
    else throw e;
}

It has these properties:

  • status: the HTTP status, or 0 for network errors, timeouts and failed deployments.
  • code: a short code such as validation_failed, unauthenticated, forbidden, not_found, rate_limited, confirm_required, deployment_failed or timeout.
  • quote: with confirm_required, the price the API quoted (show it, then call again with { confirm: true }).
  • retryAfter: on a 429, seconds until the call is allowed again. Short waits are retried for you; longer ones fail at once with this set.
  • message: a readable description, including field errors.
  • errors: field errors of a 422, as { field: ['message'] }.
  • body: the response body. A 403 for something Hostwolf does for you (like adding servers) carries a docs link.
  • Shortcut flags: isNotFound, isRateLimited, isValidation, isUnauthorized and isConfirmRequired.

Raw requests#

If an endpoint has no wrapper yet, call it directly:

await hw.http.get(`/applications/${uuid}/scheduled-tasks`);
await hw.http.post(`/applications/${uuid}/scheduled-tasks`, { name: 'cleanup', command: 'npm run cleanup', frequency: '0 3 * * *' });

Paths are relative to /api/v1. See the API reference for every endpoint.

Releases#

FileUse
https://app.hostwolf.net/dl/hostwolf-apps-sdk-<version>.tgzA fixed release. Use this in package.json.
https://app.hostwolf.net/dl/hostwolf-apps-sdk.tgzAlways the newest release. Package managers cache by URL, so prefer versioned URLs.
https://app.hostwolf.net/dl/index.jsonCurrent versions, with SHA-256 and npm integrity hashes.

To upgrade, install the newer versioned URL.

License#

MIT

Stuck, or missing an endpoint? Tell us at app.hostwolf.net/contact or [email protected].