Case study
How AppliHawk
is built
A production job-search SaaS — the architecture, the AI cost control, and the trade-offs behind it. Designed, built, tested, and operated end-to-end by one engineer.
- 620
- backend tests
- 52
- live AI tests
- 100
- frontend tests
- 5
- E2E browsers
- ~$0.01
- AI cost / user·mo
- 6
- MCP tools
- 118
- pinned dependencies
- 1
- engineer
The system at a glance
Every box below is operated by one person — which is the point of most decisions on this page.
The decision log
Six decisions, in the order the system was reasoned about. Each one names the constraint it answers and the trade-off it accepts.
-
D-01 · STACK
One engineer runs everything, so the architecture must be operable by one person
Constraint Build, deploy, and on-call are all the same person. Every moving part is a pager.
Decision Async FastAPI in a single Docker image on Railway; SvelteKit compiled to static files on Cloudflare Pages. Typed end to end — Pydantic on the wire, TypeScript strict in the client.
Trade-off No server-side rendering. Public pages are prerendered; the app itself is a pure SPA.
A static frontend means zero frontend servers to operate, patch, or scale. The entire backend surface is one container plus a worker.
-
D-02 · AI COST
AI features must never produce a surprise bill
Constraint LLM calls are the only cost that scales with user behaviour rather than user count.
Decision Scoring is on-demand only — a user clicks, a score happens, nothing fires ambiently. Per-user daily quotas, plus an admin cost dashboard with spending alerts and hard caps that fail closed.
Trade-off The product feels deliberate rather than magical. That is acceptable.
Gemini Flash-Lite keeps the marginal cost near $0.01 per active user per month — the caps exist for the day that assumption breaks.
-
D-03 · FAIRNESS
AI that scores job candidates has to be built like it will be audited
Constraint Suitability scoring brushes against employment-discrimination territory (EEOC).
Decision Scoring prompts were audited: protected-class proxies stripped from inputs, scores framed as advisory with explicit UI disclaimers. All AI input passes through sanitisation and hardened system prompts (prompt-injection defence).
Trade-off Less flattering scores than a naive prompt would give. More defensible ones.
The scoring explains its reasoning per dimension — skills, experience, location — so users can disagree with it intelligently.
-
D-04 · PRIVACY
Job-search data is sensitive, so leaving must be as easy as joining
Constraint Users trust the app with their employment situation. GDPR is the floor, not the ceiling.
Decision One-click JSON export of everything (Art. 20). Account deletion schedules a 30-day recovery window, then a background job purges for real (Art. 17). Resumes are parsed into a structured profile and the file is discarded — never stored.
Trade-off Re-parsing requires re-uploading. A file that is not kept cannot leak.
Consent is tracked per purpose, and the deletion purge is a scheduled worker task — not a manual promise.
-
D-05 · MCP
AI assistants are becoming a user surface, so the tracker speaks MCP
Constraint Connecting Claude to user data safely means real authorization, not API keys in config files.
Decision A full OAuth 2.1 authorization server in front of an MCP endpoint: six tools (read and write), scope-separated consent, per-user enablement that defaults to off, DNS-rebinding allowlist, per-user rate limits.
Trade-off Shipped dormant — while unconfigured, the endpoint does not mount at all. Zero attack surface until it earns its keep.
"Ask Claude to move the Stripe application to Interview" is the demo. The OAuth server behind it is the engineering.
-
D-06 · OPERABILITY
Failures surface to one phone, so deploys have to be boring
Constraint There is no ops team, no QA team, and no second opinion at 2am.
Decision 620 backend tests plus 52 integration tests against the live LLM. 100 frontend tests, E2E across five browser targets. A compiled dependency lockfile, real health checks (DB and Redis, not just 200 OK), and startup validators that refuse to boot with production misconfiguration.
Trade-off Feature cadence is slower. Deploys are boring. Boring is the goal.
The startup validators are the favourite: a prod deploy with a placeholder secret or localhost CORS origin refuses to start, loudly.
Work with me
Need something like this built?
Everything on this page — product, backend, frontend, AI integration, and the operations around it — was built by me. If you need a production-grade tool, an AI feature done responsibly, or a full product taken from idea to deploy, I'm available for freelance work.