Marshal
A self-hosted job-application workspace I can use from my laptop and my phone: capture a job ad, check the fit, draft material that only says what I can back up, and track each application to an offer.
Designed and built end to end · Personal project · 2026 · Pre-alpha · Private repository
Why it exists: the tool I already had only ran on one Mac
Job Search Dispatch, my earlier tool, proved the idea in daily use: record experience once, let a model read a job ad against it, and draft application material that stays inside what I have confirmed. But it lives on one Mac. It runs as a macOS service and keeps working state in that machine's browser, so a job I found on my phone during a long day out had to wait until I was back at the desk.
Marshal rebuilds the idea for that gap, and for a second one. Friends asked to use the earlier tool, and I did not want their résumés on my server or mine on theirs. So Marshal is a single-person instance by design: I run one for myself, reachable only from my own devices, and anyone else can run their own copy from the same repository.
It is a clean-room successor. Marshal takes over the earlier tool's concepts, not its code or data: every module is rewritten, and nothing from the old data directory, history or fixtures is copied.
One application, from a phone capture to an offer
- 01
Capture where I am
Paste a job ad into the phone or the laptop. The ad is capped at 50,000 characters and an optional source link is kept as plain text. The capture is saved to the server so the other device can load it.
Paste → job record → explicit Save
- 02
Decide whether it is worth the time
Triage matches the ad's requirements one by one against my confirmed experience and quotes the ad for each requirement; a quote that cannot be found in the ad is not accepted. Hard requirements I record myself, such as work rights, are checked by plain code and never sent to the model. The result is a decision of pursue, explore, clarify or skip, with a suggested time budget.
Confirmed claims + quoted requirements → decision card
- 03
Draft, check and accept
For a job worth pursuing, Marshal plans the letter from the triage result and the model writes inside that plan. Each sentence is then checked against the facts it cites: a number or a name those facts do not contain is flagged. A separate audit runs unless I waive it, and the waiver is recorded. I accept the letter or discard it.
Letter plan → draft → sentence check → audit → acceptance
- 04
Download the documents
The accepted letter renders to a one-page A4 PDF and the tailored résumé to two pages, using a headless Chromium inside the container with network access blocked during rendering. A download is refused when its source has changed since it was accepted.
Accepted text → HTML template → A4 PDF
- 05
Track it to the end
A board follows each application through Not applied, Applied, Interviewing and Offer, and a due queue lists the follow-ups that are due today or overdue. Status changes are appended to a history rather than overwritten. I submit every application myself.
Pipeline board → due queue → manual submission
Decisions I can defend
One instance per person, not a shared service
Keeping several people's résumés apart would touch every store, route, cache and backup, and one mistake would leak someone's career history. With one instance per person there is no cross-user isolation to get wrong, and friends do not have to trust my server.
Tradeoff accepted: Each person has to run Docker and bring their own model API key. There is no sign-up page and no hosted option.
Reachable only over a private network
The container publishes to the host's loopback address only, and Tailscale serves it over HTTPS to devices on my own tailnet. No port is open to the internet, so there is no public login to build or defend.
Tradeoff accepted: Every device I use has to be on the tailnet, and a container still shares the host's kernel. The boundary I rely on is at the application, credential and storage level.
Explicit save and load between devices, with revisions
Each device works on its own copy and saves against a revision number. A save from a device that is behind is refused with a conflict, both versions are kept, and I choose. The server keeps the last 20 revisions of each document.
Tradeoff accepted: It is not live sync. I press Save on one device and Load on the other, and a conflict asks me a question instead of merging for me.
Remove unconfirmed experience on the server, before the prompt exists
The model reads a filtered projection of my profile, never the profile itself. A claim is included only if I have confirmed its current wording, it is cleared for that use and it has not expired. Prompts are assembled on the server, so an out-of-date browser cannot widen what the model sees.
Tradeoff accepted: Recording and confirming claims takes effort, and editing a claim means confirming it again. The sentence check compares numbers and names; it does not prove a sentence means what the fact means, so I still read every letter.
Paste the job ad instead of fetching it
Fetching a URL from a server needs its own defences against being pointed at private addresses, plus page extraction that breaks when a job board changes. Pasting let phone capture and triage ship without that surface.
Tradeoff accepted: There is no fetch-from-link button yet. The earlier Mac tool still has one.
Built to be shared
- On a laptop: clone the repository, copy the example environment file and run one Docker Compose command. The app listens on the machine's loopback address only.
- On a small server: one script sets the allowed origin, builds the image, waits for the health check and puts the instance behind Tailscale. Backup and restore scripts are included, with optional encryption for the archive.
- Model access is bring-your-own-key for an Anthropic or OpenAI-compatible endpoint. The key stays in the server's environment and is never sent to a browser.
- Profile, résumé and working state are plain JSON files in one data directory that the owner can read, back up and compare.
- The repository ships fictional examples only. Real profiles, résumés, keys and rendered PDFs are ignored by Git, and a secret scan runs over the full history on every push.
- The licence is MIT. The repository is private while the project is pre-alpha, so the sharing path is designed and documented rather than proven by other users.
My own instance
Mine runs on a Google Cloud e2-micro virtual machine in Sydney with 1 GB of memory. I measured the server at about 55 MiB when idle and about 150 MiB while rendering a two-page PDF, which is why a machine that small is enough; swap is there for image builds, not for rendering.
The container runs as a non-root user with a read-only root filesystem, and renders one PDF at a time to keep memory predictable. I checked the save, load and conflict flow from a real iPhone over Tailscale on 30 September 2026.
What I verified
GitHub Actions runs four checks on every push: type checking, 220 unit and contract tests, the browser journeys on phone and desktop viewports, and a container build that renders a multilingual PDF and inspects its page count, page size and embedded fonts. A fifth job scans the full history for secrets. The run for the pipeline release on 2 October 2026 passed.
The roadmap records 58 browser journeys passing at that release, including a phone-sized run from capture to both PDF downloads. The journeys use a stubbed model, and no test calls a paid model.
The image on this site is composed from two captures saved by those browser journeys, one desktop and one phone. Every role and company in it is fictional test data.
Current scope and limitations
- Marshal is pre-alpha and has one user, me. It was started on 29 September 2026. No adoption, interview-conversion or time-saved result is claimed.
- The source repository is private. Nobody else has self-hosted it yet, so the setup guide has only been exercised by me.
- The tests establish that the rules hold with a stubbed model. They do not establish the quality of what a live model writes.
- The sentence check is a deterministic comparison of numbers and names against cited facts, not a judgement of meaning. Final review remains mine.
- There is no automatic submission, no fetch-from-link capture, no live sync and no multi-user mode. Model calls leave the server for the configured provider.