Skip to content

Backup File Sentry: Engineering a Backup Agent for Field Conditions

Software behaves impeccably on the machine that built it. The interesting failures start afterwards, in front of somebody who did not write it and reports the problem as “it didn’t work”. Backup File Sentry is a NotAnother tool for that setting: a command-line agent in TypeScript on Bun that moves configured files between a client machine and an FTP server, both ways. Moving the bytes is the easy part; the harder question is what happens to the program once nobody who understands it is in the room.

Challenges

An agent running where its authors are not fails quietly, then invisibly. A run that stops halfway leaves no account of how far it got; a double-clicked executable on Windows prints something useful and closes its window before anyone reads it; one that keeps every copy it ever made fills the disk it protects.

Our Solution

Every transfer is a job, and every job is a row in a local SQLite database managed through Drizzle ORM. The row carries a UUIDv7 identifier, the configuration and file it targets, its direction, a status, and a JSON payload of numbered, named phases. The service appends a phase at each milestone — connection details fetched, local backup written, transfer completed — and saves the row each time. If the process dies mid-run, the last phase written is the last phase reached; if it throws, an Error phase carrying the message is appended and the job marked failed. The record becomes a narrative rather than a verdict: not merely that the backup failed, but that it reached its third phase, wrote its local copy, and then could not connect.

Retention is bounded at both ends. Backup names carry a millisecond timestamp prefix, so sorting a listing lexically sorts it chronologically: ten copies are kept locally in a folder named for the file’s code, five in the remote directory of that same code. Downloads took a later correction — the agent copies the local file into the backups tree, then deletes it before pulling the newest remote version into its place, safe only because the copy exists. An empty remote directory records a “Nothing to download” phase and completes: nothing to restore is not a failure.

Diagnosing from a Distance

The most instructive part of the history is five commits made inside seventeen minutes of one another, clustered around getting the tool to behave on somebody else’s machine. A --verbose flag was threaded from the command line to the FTP client’s own verbose mode, so protocol chatter can be switched on for one run with no rebuild. Job errors were caught at the CLI route and logged, so a failure reports itself instead of ending the process. And a prompt reading “Press Enter or Ctrl+C to exit…” was placed after every transfer, outside the try/catch so it runs either way, keeping the window open.

Then came a job list command, returning the five most recent jobs newest-first with their phase payloads — added, its commit message says, to better debug issues on the client. Asking an operator to run one command and read back what it prints needs no remote access.

Shipping to the Machine

Bun does not need installing on the target machine: distribution is a self-contained executable from bun build --compile, runtime included, applying its own schema through an init command that runs the migrations shipped beside it. Configuration is held as data — FTP endpoints in a configs table, file references in a files table — so a job is invoked by identifier, credentials are read from the row, and the same binary serves every machine, its local database supplying the particulars. Eight targets are built: Windows, macOS and Linux on x64, macOS and Linux on arm64, plus baseline variants of the three x64 builds for older processors lacking newer instruction sets, because the field contains hardware nobody chose recently.

Conclusion

Underneath, the tool is orderly: a shared core of entity, mapper, repository, service and controller repeated, in whole or in part, across the feature modules, wired by a hand-rolled dependency injection container of some 120 lines, an SFTP client sitting unregistered beside the FTP one. But the parts that decide whether an unattended tool can be trusted are simpler than the architecture: a job history that says how far the run got, a window that stays open long enough to be read, one command reporting the last five runs, and an executable that carries its own runtime.

Photograph of a metal filing cabinet with one drawer pulled open, packed with densely filed index cards

More use cases