npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@frontierjs/outpost

v0.1.3

Published

The process a fleet server runs — Basecamp's hands on a machine

Readme

@frontierjs/outpost

The process a fleet server runs. Basecamp is the control plane; this is its hands on a machine. One per Server row (FJS-D29 — infrastructure takes place nouns), and it does two things and nothing else:

  • answers commands — pull, build, deploy, stop, health-check, exec, prune — every one of them signed;
  • reports — a heartbeat that says where to reach it, and the machine's volumes and disk on a slower clock.

It is not an FJS application. Its job is to run Docker commands and report health, and an FJS app would put a schema, a migration runner and an ORM on every fleet server to do that.

Running one

OUTPOST_SERVER_ID=<the Server row's id> \
OUTPOST_SECRET=<this server's secret> \
BASECAMP_URL=https://basecamp.internal \
OUTPOST_TLS_CERT=/etc/basecamp/outpost.crt \
OUTPOST_TLS_KEY=/etc/basecamp/outpost.key \
bunx --bun @frontierjs/outpost

Those five have no default and the process refuses to start without them. An Outpost that cannot name its server reports as nobody. One with no secret would either refuse every command or accept every one. The command port serves TLS only. Its certificate is self-signed (ensureCert in @frontierjs/outpost/cert), sent to Basecamp in the enrollment exchange and pinned there, so a command reaches no port holding any other (FJS-D557). Everything else has a default — OUTPOST_PORT (8180 dev, 7180 test — the number comes from packages/cli/core/ports.js, project id 8), OUTPOST_PUBLIC_URL, OUTPOST_HEARTBEAT_MS, OUTPOST_REPORT_MS, OUTPOST_WORK_DIR, OUTPOST_RESTORE_DIR (/var/lib/outpost/restore, the one directory /volume/restore reads an archive from), OUTPOST_BACKUP_DIR (/var/lib/outpost/backups, where /backup writes one), and the four that belong to the static half — OUTPOST_STATIC_DIR, OUTPOST_STATIC_PORT (8181 dev, 7181 test; 0 turns it off) and OUTPOST_STATIC_URL — and OUTPOST_CADDY_ADMIN, the machine's Caddy admin API (http://127.0.0.1:2019), which a /deploy naming hosts pushes its route to. A machine with no Caddy deploys an app with no hostname as before.

On the box a Basecamp runs on, fli deploy also writes OUTPOST_HOST_DB_PATH and OUTPOST_HOST_ENV_PATH: the database directory and the file holding ENCRYPTION_KEY. posture() then stats both into host_files, which /hub/security/ grades. No other machine sets them.

OUTPOST_PUBLIC_URL is stated rather than derived, because this process cannot see the address the world reaches it at. It must be https, and the process refuses to start otherwise: Basecamp registers no target for a plain-http one. It is what the heartbeat registers as the Conduit target, and until that lands Basecamp refuses every release for the machine — with a message saying so, rather than a green deploy that ran nothing.

Taking one off

The install (Basecamp's installScript()) writes /usr/local/sbin/outpost-uninstall beside the unit, so a box carries its own way back whatever happened to the Basecamp that enrolled it. sudo outpost-uninstall removes the Outpost, its credential and its firewall, and leaves the apps it placed running. --purge also removes those apps and every Docker volume, /var/lib/outpost with its backups, Docker, Caddy, their apt sources and the Outpost's bun. It asks you to type purge, or takes --yes when there's no terminal. Both end by listing what they left on purpose: the sshd drop-in (removing it can turn password login back on), chrony, security updates, and the Server row, which stays in Basecamp until a person removes it there.

The protocol

Basecamp reaches it at outpost:<server-id> over Conduit. Every route but GET /health requires a signature; /exec runs a shell command as this process's user, so the default is refuse and a route opts out rather than in.

| | | | --- | --- | | POST /pull | { image } → { digest } | | POST /deploy | { deployment_id, app_id, image, digest, source, config } → { containerId, digest, commit_sha }; config is port, containerPort, volumePath, cpuLimit (--cpus), memLimitMb (--memory), env and logs | | POST /stop | { app_id } → { stopped, unrouted } | | POST /site/preview | { app_id, source, branch, alias, cloudflare: { token, account_id } } → { commit_sha, url, version_url }; the branch's head installed, built with site-kit build --preview and uploaded as a Worker version aliased alias, each step in a throwaway container, the checkout removed after; commit_sha is what was built, which may be newer than the caller saw | | POST /route | { app_id, hosts, port? } → { hosts, port, rebind }; the app's Caddy route replaced, or removed for no hosts, dialing the port the running container published. rebind is a sentence when the bind no longer matches, which only the next /deploy changes. A stated port is for an app with NO container — a preview's dev server on this machine — and is refused beside one | | POST /health-check | { app_id, port, path } → { healthy }, plus reason when it is not; with a path, running is not enough — 127.0.0.1:<port><path> must answer 2xx | | POST /exec | { command, timeout_s } or { step } → { exit_code, stdout, stderr } | | POST /logs | { app_id, tail, since } → { running, tail, since, stdout, stderr }, plus error when there is no such container | | POST /static/publish | { app_id, files } → { digest, files, bytes } — written, not yet live | | POST /static/activate | { app_id, slug, digest } → { digest, host, url } | | POST /static/health | { app_id, digest } → { healthy, digest }, plus reason when it is not | | POST /static/retire | { app_id, slug } → { retired } — off the air, every release kept | | POST /static/releases | { app_id } → { current, digests } | | POST /ingress | {} → { caddy, listen, routes: [{ id, hosts, to }], servers } — what Caddy's ingress server routes, whoever wrote it, each route reduced to its destinations; servers lists every other http server (a second one on :443 is a Caddyfile left running); caddy: false on a machine without one | | POST /system/prune | { targets, keep_images } → { freed_bytes, removed, volumes, usage } | | POST /volumes/prune | { names } → { removed } | | DELETE /volumes/<name> | → { removed }, or 409 with the container holding it | | POST /backup | { app_id } → { volume, path, bytes, files }; fjs-<app>-data tarred (gzip) into OUTPOST_BACKUP_DIR by a throwaway container holding the volume read-only. The archive stays on the machine and the answer is its path. Refused for a volume the machine does not hold | | POST /volume/restore | { app_id, path, overwrite? } → { volume, archive, files, bytes, overwrite }; a tar (gzip or not) under OUTPOST_RESTORE_DIR, extracted into fjs-<app>-data. Refused while any container holds the volume, and into one with files unless overwrite: true, which empties it first | | GET /health | unsigned liveness — says nothing about the machine |

The signature is @frontierjs/toolbelt/signature — the same module Conduit signs with and Basecamp verifies with. Method, path, timestamp, nonce and a hash of the body, so a captured signature cannot be replayed, moved to another endpoint, or kept while the body is swapped underneath it.

Where the bytes come from

V1 builds on the target: a source.kind === 'git' deploy clones, builds, and answers the digest of what it built, which Basecamp records on Deployment.builtImage and addresses every later step by. That is honest while the Outpost is co-resident with Basecamp — one machine, the CapRover shape. The end state is build-once-promote-a-digest, which needs a builder and somewhere to put the artefact: IDEAS/deploy-plane.md.

Only sha256:<64 hex> counts as a digest. A tag is a name, and two builds share it — an image inspect that answers anything else is reported as no digest at all rather than as a plausible one.

Or the bytes are the app

An App whose source.kind is inline has no repository and no image: the files themselves are the release. A page that loads React from a CDN is a whole app, and there is nothing here to build. POST /static/publish writes the files into a directory named for their own sha256 and POST /static/activate swaps one symlink — which is why a republish of identical bytes answers the same digest, and why a rollback sends nothing at all.

The digest is computed here and the caller states none: a release records what ran, and a claim about bytes somebody sent is not a reading of the bytes that landed. Every path is an allow-list of plain file names, and the files are written into a staging directory and moved into place with one rename, so a publish that dies halfway cannot leave a release holding some of its own files.

They are served on a second listener, on a port of its own (OUTPOST_STATIC_PORT). The pages are written by whoever can edit an app, so anything they reach as same-origin is theirs — and a port is an origin, so sharing one with the signed command protocol would put the fleet inside every prototype. Nothing on that port is signed or authenticated: it is a web server for public files, and the only reads it can perform are inside one release directory. An app is reached by the first label of the Host (shop.fleet.example.com) or, where there is no DNS, by the first path segment (/shop/).

Testing

bun run test — no Docker and no network. createDocker({ run }) takes the runner and createReporter({ fetch }) takes the client, so what is asserted is what the machine was ASKED to do and what left the process. A package that could only be tested against a real daemon would be tested rarely and wrongly.