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.
Also see: REST API reference · MCP server · Changelog · Get an API token
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#
REST: Projects endpoints
| Method | What it does |
|---|---|
projects.list() / get(uuid) | Projects; get includes the environments |
projects.create({ name }) | New project with a production environment |
projects.update / delete | Rename, 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 / deleteEnvironment | Manage environments |
projects.sharedEnvs / environmentSharedEnvs | Variables shared by every app of a project or environment |
Applications#
REST: Applications endpoints
| Method | What 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 / stop | Restart 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#
| Method | What 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 / updateEnv | Strict create (409 if it exists) and update |
applications.deleteEnv(uuid, envUuid) / deleteEnvByKey(uuid, key) | Remove a variable |
teams.sharedEnvs() and friends | Team-wide variables, used as {{team.KEY}} |
Running containers see changed variables after the next deploy.
Deployments#
REST: Deployments endpoints
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.
| Method | What 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#
| Method | What 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.
| Method | What 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#
REST: Autoscaling endpoints
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 });
| Method | What 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 / update | Enlarge (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#
| Method | What 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 / deleteStorage | Change or delete one |
applications.setVolumeBackup(uuid, storageUuid, { frequency: 'daily' }) | Back up a volume on a schedule |
applications.runVolumeBackup / deleteVolumeBackup | Back up now, or remove the schedule |
Teams#
| Method | What 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 asvalidation_failed,unauthenticated,forbidden,not_found,rate_limited,confirm_required,deployment_failedortimeout.quote: withconfirm_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 adocslink.- Shortcut flags:
isNotFound,isRateLimited,isValidation,isUnauthorizedandisConfirmRequired.
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#
| File | Use |
|---|---|
https://app.hostwolf.net/dl/hostwolf-apps-sdk-<version>.tgz | A fixed release. Use this in package.json. |
https://app.hostwolf.net/dl/hostwolf-apps-sdk.tgz | Always the newest release. Package managers cache by URL, so prefer versioned URLs. |
https://app.hostwolf.net/dl/index.json | Current versions, with SHA-256 and npm integrity hashes. |
To upgrade, install the newer versioned URL.
License#
MIT