Skip to main content

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.

AppliHawk system architectureBrowsers load the static SvelteKit frontend from Cloudflare Pages and call the FastAPI backend on Railway, which uses PostgreSQL and Redis. A Celery worker handles background jobs and email via Resend. Both API and worker call Gemini. Claude connects through an OAuth 2.1 MCP endpoint on the API.Browserjob seekerCloudflare PagesSvelteKit · staticFastAPIRailway · DockerREST + /mcp (OAuth 2.1)PostgreSQLAlembic migrationsRediscache · queues · limitsCelery worker + beatreminders · digests · GDPR purgeGeminiscoring · parsing · capsResendtransactional emailClaude / MCP clientsper-user opt-in · dormantOAuth 2.1

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.