AbsoluteJS

Deploy

A deploy pipeline for Bun projects on your own servers. A Target is anywhere you can run a command and copy a file — a DigitalOcean Droplet over SSH, a Linode box, your own laptop. Two ops, four words: exec and upload. Zero ssh2 npm dependency — the bundled sshTarget shells out to the system ssh + rsync binaries.

#Quick Start

The default pipeline for a Bun project on Linux: prepare → upload → install → build → link → restart → verify. Releases live in releases/<id>/, a current symlink swaps atomically, rollback(id) re-points the symlink and restarts without re-uploading.

TS
import { createDeployer, sshTarget, systemdManager } from '@absolutejs/deploy';

const deployer = createDeployer({
  appName: 'my-app',
  target: sshTarget({
    host:     'droplet-1.example.com',
    user:     'deploy',
    identity: '~/.ssh/id_ed25519',
  }),
  source: { kind: 'directory', root: './' },
  env:    { PORT: '3000', DATABASE_URL: process.env.DATABASE_URL! },
  processManager: systemdManager({ user: 'deploy' }),
  verify: { kind: 'http', url: 'http://localhost:3000/health' },
});

const release = await deployer.deploy();
console.log(`Deployed ${release.releaseId} in ${release.durationMs}ms`);

#Infrastructure Providers

The normalized InfrastructureProvider contract covers declared capabilities, node identity and state, inventory, idempotent provisioning, termination, and regional placement across five clouds.

ProviderImportSurface
DigitalOcean@absolutejs/deploy/digitalocean-infrastructureDroplets and regional placement
Google Cloud@absolutejs/deploy/gcpImmutable templates and managed operations
Hetzner@absolutejs/deploy/hetzner-infrastructureCloud servers and locations
Linode@absolutejs/deploy/linode-infrastructureInstances and regional placement
Vultr@absolutejs/deploy/vultr-infrastructureInstances and regional placement

#Release control plane

Releases are immutable inputs whose transitions remain observable and recoverable.

  1. Artifact
    Create or stream an immutable artifact with integrity metadata.
  2. Stage
    Upload into a versioned release directory without mutating current.
  3. Verify
    Install, build, and verify before publication.
  4. Publish
    Atomically move the current pointer and restart.
  5. Operate
    Retain evidence, stop superseded releases, or roll back by id.

#Global edge ingress lifecycle

Global ingress normalizes provider resources while preserving TLS termination at the regional edge.

  1. Desired state
    Declare listeners, health checks, and ordered regional backends.
  2. Reconcile
    Construct DigitalOcean or GCP provider resources idempotently.
  3. Converge
    Wait for provider operations before exposing dependent resources.
  4. Observe
    Return normalized addresses, state, and provider references.

#Managed preview lifecycle

Managed previews make temporary environments explicit resources with ownership and garbage collection.

  1. Request
    Bind a commit, artifact, owner, expiry, and idempotency key.
  2. Provision
    Provision ephemeral infrastructure and encrypted storage when required.
  3. Publish
    Deploy, verify, publish DNS/TLS, and expose the preview URL.
  4. Collect
    Reconcile expiry or closure and clean every provider resource.

#Targets

A Target is just { exec(cmd, opts?), upload(local, remote, opts?), close?() }. Two are bundled. Provider-native control planes that do not fit the exec-and-upload shape use typed adapters and package entry points, while infrastructure providers expose a shared reconciliation contract.

TS
// localTarget — runs in a local directory. Tests + same-box workflows.
import { localTarget } from '@absolutejs/deploy';
const target = localTarget({ root: '/srv/my-app', env: { ... } });

// sshTarget — shells out to system ssh + rsync. Zero ssh2 dep. Works
// against ANY VPS where the controller machine has ssh + rsync in PATH
// (mac / linux / WSL all do).
import { sshTarget } from '@absolutejs/deploy';
const target = sshTarget({
  host:       'droplet-1.example.com',
  user:       'deploy',
  port:       22,
  identity:   '~/.ssh/id_ed25519',
  rsync:      true,                  // false → scp fallback
  sshFlags:   ['-o', 'ServerAliveInterval=30'],
});

// A Target is just { exec(cmd, opts?), upload(local, remote, opts?), close?() }.
// Anything that satisfies that contract is a valid target. Provider-specific
// adapters that DON'T fit (Cloudflare Workers API, Fly Machines API, AWS
// Fargate task-run) ship as sibling packages.

#Pipeline

The default pipeline is a plain array of DeployStep objects — splice in your own, drop ones you don't want, replace the lot. Each step receives a DeployContext with { target, source, releaseId, releasePath, currentPath, env, hooks, annotations, dryRun }.

TS
// Default pipeline for a Bun project on Linux:
//   prepare → upload → install → build → link → restart → verify
//
// Override entirely or splice your own step in:
import { defaultBunPipeline, type DeployStep } from '@absolutejs/deploy';

const customSteps: DeployStep[] = [
  ...defaultBunPipeline(),
  {
    name: 'warm-cache',
    run: async (ctx) => {
      await ctx.target.exec(`curl -s http://localhost:${ctx.env.PORT}/warm-cache`);
    },
  },
];

createDeployer({ /* ... */ steps: customSteps });

#Process Managers

How files-on-disk become a running process is pluggable. bareManager is the zero-dep default (nohup + pid file). systemdManager generates a templated unit pointing at current/ and runs the daemon-reload + restart dance. Anything that implements { reload, stop?, status? } against a Target works — wrap PM2, supervisord, runit, or @absolutejs/runtime as needed.

TS
// bareManager (default) — 'nohup bun run start &' + pid file. Zero
// remote dependency.
import { bareManager } from '@absolutejs/deploy';
createDeployer({ /* ... */ processManager: bareManager() });

// systemdManager — generate + install a systemd unit pointing at current/.
// daemon-reload + restart. The production answer for VMs.
import { systemdManager } from '@absolutejs/deploy';
createDeployer({
  /* ... */
  processManager: systemdManager({
    user:      'deploy',
    group:     'deploy',
    execStart: '/usr/local/bin/bun run start',
    restart:   'always',
  }),
});

// Or implement your own (PM2, supervisord, runit, @absolutejs/runtime):
const customManager: ProcessManager = {
  reload: async (target, ctx) => { /* ... */ },
  stop:   async (target, ctx) => { /* ... */ },
  status: async (target, ctx) => 'running',
};

#Verify

HTTP / TCP / custom probes with retries. The deploy is only successful after verify passes — a green deploy that nobody can reach is a yellow deploy. Recommend always wiring one.

TS
// HTTP verify — most common.
verify: {
  kind: 'http',
  url: 'http://localhost:3000/health',
  retries: 30,
  intervalMs: 1_000,
  expectStatus: 200,
};

// TCP verify — for non-HTTP services.
verify: { kind: 'tcp', host: 'localhost', port: 3000, retries: 30 };

// Custom verify — escape hatch.
verify: {
  kind: 'custom',
  check: async (ctx) => {
    const result = await ctx.target.exec('healthcheck.sh');
    return result.exitCode === 0;
  },
};

#Annotations, Dry-Run, Resume

Per-release metadata persists as releases/<id>/.deploy-meta.json: commit SHA, ref, message, author, arbitrary tags. dryRun: true logs the plan without mutating the target — verify pipeline shape from CI before flipping a real current symlink. If a deploy fails on verify (slow health check) but the release is intact on disk, resumeReleaseId restarts from the dead step.

TS
// Per-release metadata stored at releases/<id>/.deploy-meta.json.
// Surfaces on DeployResult.annotations and via deployer.readReleaseMeta(id).
const release = await deployer.deploy({
  annotations: {
    commitSha: 'abc1234',
    ref:       'refs/heads/main',
    message:   'fix: handle null in checkout',
    author:    'alex@example.com',
    tags:      { ci: 'github-actions', env: 'production' },
  },
});

// Resume a failed release — restart from the dead step. Useful when the
// deploy fails on 'verify' (slow health check) but the release is intact
// on disk.
const resumed = await deployer.deploy({ resumeReleaseId: failedReleaseId });

// Dry-run — log the plan without mutating the target. Verify pipeline
// shape from CI before flipping a real 'current' symlink.
await deployer.deploy({ dryRun: true });

#Rollback & Prune

Rollback re-points current at a previous release and restarts — no re-upload, no re-install, no re-build. prune({ keep: N }) drops the oldest releases. Every deploy + rollback auto-cleans a dangling current.next from a prior crash.

TS
// Rollback — re-points 'current' at a previous release and restarts.
// No re-upload, no re-install, no re-build. Fast.
const previous = (await deployer.listReleases()).at(-2);
if (previous) await deployer.rollback(previous);

// Housekeeping — keep last N releases on disk.
await deployer.prune({ keep: 5 });

// Inspect a release's metadata.
const meta = await deployer.readReleaseMeta(releaseId);
// → { releaseId, annotations, status, failedStep?, completedSteps, ... }

Continue toward an outcome

These playbooks show where this package fits, how to verify the combined system, and what changes before production.

Current package surface

What ships today

@absolutejs/deployv0.21.0 · betaPlatform & InfranpmSource
26entry points210symbols

Import surface · click to copy

43 symbols
ExecOptionsexportPermalinkSource
TS
ExecOptions
Exported from @absolutejs/deploy
Use this API in an outcome:Ship a SaaS platformProve a safe release

Outcomes

What you can build

Automate complete releases

Describe targets, process managers, pipelines, verification, streamed artifacts, environment synchronization, DNS, TLS, and global edge ingress through typed provider seams.

Keep infrastructure replaceable

Provision or reuse infrastructure from DigitalOcean, Hetzner, Cloudflare, and other adapters without coupling the release model to one provider.

Hardening checklist

Production guidance

Make releases reproducibleUse immutable release artifacts, explicit target identity, idempotent provision-or-reuse operations, secret-aware environment propagation, certificate renewal, and post-release verification.

Follow in order

Troubleshooting path

1
A release is not healthy
Run the verify stage against the target and inspect streamed release evidence, process-manager state, environment propagation, DNS, TLS, and provider-specific provisioning output.