← Back to Projects

AI Apply

Multi-tenant web app that scrapes job boards, drafts a tailored cover letter per role using Claude, and surfaces every draft in a review-and-send UI.

Year 2026 – Present
Platform Web · Azure
Role Architect / AI Orchestrator
Repo GitHub ↗

Fully AI-Orchestrated Build

AI Apply is a multi-tenant SaaS designed to remove the most repetitive part of a job hunt. A user signs in, points the app at their CVs, defines one or more job-search criteria, and clicks Run. A background worker scrapes the chosen boards (Seek AU/NZ, Wanted KR), picks the best CV per listing, asks Claude to draft a tailored cover letter, and stores each one as an editable draft in the Apply UI. Drafts never leave the app — there is no auto-submit; the candidate still reads, edits, and marks each one sent.

The entire codebase — FastAPI backend, SQLAlchemy data model, vanilla-JS frontend, worker pipeline, three OAuth flows, Stripe billing with idempotent webhook crediting, envelope-encrypted secret store, Bicep infrastructure templates, and the GitHub Actions deploy pipeline — was orchestrated through Claude. Architecture, invariants, and product direction were author-led; every line of implementation was AI-driven. The one piece that was hands-on was provisioning and wiring the Azure environment itself: resource group, App Service, Postgres Flexible Server, Key Vault, container registry, federated identity for the deploy workflow, and OAuth client registrations across GitHub, Google, and Microsoft Entra.

FastAPI + Background Pipeline

FastAPI API Layer

One router per concern — auth, settings, runs, applications, billing — with a single current_user dependency that handles both real OAuth sessions and the local-dev auto-login path. The static frontend mounts alongside; no build step.

Worker Pipeline

Framework-free orchestrator: scrape → dedupe against the user's job table → load CVs from the upload table or GitHub → call Claude with a strict cover-letter contract → debit tokens → save draft. Lives in BackgroundTasks today; the orchestrator is self-contained so it can be lifted onto Container Apps Job or Storage Queue without touching the API.

Azure Hosting

App Service (Linux Python) running gunicorn, Postgres Flexible Server, Key Vault for user secrets via the App Service managed identity, ACR for container builds. All provisioned by a single Bicep template; deploys on push to main via federated-identity GitHub Actions.

Local Dev Mode

Same codebase runs single-user against SQLite with no auth, an envelope-encrypted secrets sidecar, and (optionally) the local claude binary so generations consume the developer's Claude subscription instead of an API key. One command: python main.py.

Three Billing Modes, One Worker

  • Tokens (hosted default) — Anthropic calls go via the host's API key; per-call cost is debited from the user's centitoken balance. Stripe Checkout funds top-ups; the webhook credits idempotently against a unique stripe_session_id so replays and concurrent /checkout/verify calls can't double-credit.
  • BYOK — the user supplies their own sk-ant-... key. Stored envelope-encrypted (Fernet locally, Key Vault in production) behind an opaque ref; no balance touched.
  • System (local only) — generation routes through the local claude binary as a subprocess. Rejected by the API in hosted mode.

Token balances are stored as integer centitokens (×100) and gated by a credit/debit ledger so every change is auditable. Webhook idempotency, race-safe credit, and the local/hosted gate are all covered by tests in the pytest suite.

Invariants & Implementation

  • Single mode flag (SECRETS_BACKEND != "azure-key-vault") drives every local-vs-hosted behaviour: auth, billing, generation source, secrets backend. One source of truth, exposed to the frontend via GET /api/config.
  • OAuth across three providers — GitHub, Google OIDC, and Microsoft Entra email/password — with email as the canonical account ID and signup-provider lock to prevent account takeover via a different login route.
  • Cover-letter contract bans markdown and meta-commentary but deliberately does not blacklist "Claude" or "Anthropic" — those legitimately appear in letters for jobs at Anthropic; the human review step is the safety net, and a regression test enforces the boundary.
  • User secrets (Anthropic key, GitHub token) never sit in the DB as plaintext — only an opaque ref like kv:anthropic-42. The SecretStore abstraction has Fernet (local) and Key Vault (production) implementations behind a common interface.
  • Background runs are recovered after restart via a boot-time recover_orphaned_runs() sweep over live-status rows so a process death mid-run can't strand a user's batch.
  • Two-phase scraper architecture (per-site adapters behind a single registry) already supports Seek AU/NZ and Wanted KR; adding a new site is a four-file change.
Python 3.11 FastAPI SQLAlchemy 2.0 Anthropic SDK Azure App Service Postgres Key Vault Bicep Stripe OAuth / OIDC Pytest GitHub Actions

What Was Hands-On vs Claude-Driven

The project was a deliberate test of running a complete shipped product end-to-end through AI-orchestrated development — not just code generation, but architecture decomposition, invariant enforcement, test-suite design, deployment scripts, and the CI pipeline that builds and ships it.

  • AI-driven (everything else): the FastAPI app, SQLAlchemy models, worker pipeline, scraper integration, Anthropic SDK call, Claude CLI subprocess path, three OAuth flows, Stripe Checkout + webhook, token ledger, envelope-encrypted secret store, vanilla-JS frontend, pytest suite, Dockerfile, gunicorn entrypoint, Bicep templates, and the GitHub Actions deploy workflow.
  • Hands-on (Azure setup): standing up the resource group, configuring App Service / Postgres / Key Vault / ACR, registering the OAuth applications across GitHub, Google, and Entra, wiring federated identity for the deploy workflow, and verifying that managed identity correctly resolves against Key Vault.
  • Author-led (across both): architecture, invariants (anonymity contract, billing idempotency, secret handling, the local-vs-hosted gate), product scope decisions, and review of every change before merge. The repo's CLAUDE.md encodes those invariants so AI agents enforce them on every change rather than re-discovering them.