A modular-monolith Rust engine for a developer career platform: GitHub-driven resumes, ATS scoring, portfolios, and hybrid search.
Live: devresume-api.salmondune-b6d2a6eb.centralindia.azurecontainerapps.io
DevResume AI is the backend for a "developer career operating system": connect a
GitHub account, let the platform sync and analyze the repos (language, framework,
database, cloud, and CI detection from the actual manifest files), and turn that
raw activity into resumes, ATS scores, portfolios, and career recommendations β
with hybrid keyword + vector search across all of it. It's a Rust/Axum modular
monolith: ~20 workspace crates with clean domain boundaries (see ADR 0004),
one Postgres database with pgvector for embeddings, deployed as a single
container on Azure.
This is a separate, independently-developed product from the rest of this
monorepo's Node/Next.js resume pipeline (resume-core / resume-admin) β the
multi-user, API-first successor in progress, not a rewrite of it.
This is an actively-developed backend, not a finished product, and this README
says so plainly rather than describing the target architecture as if it already
shipped. Every feature below is tagged with its actual state, checked against
the source (apps/, crates/, migrations/) β not against the design docs in
docs/, which were written ahead of implementation and are ahead of what's
actually wired up today.
| Tag | Meaning |
|---|---|
| β Shipped | Real logic, backed by the database or an external API call, reachable over HTTP today |
| π§ Engine built | The domain logic exists as a tested library in its crate, but no HTTP route calls it yet |
| π§ͺ Mocked route | The HTTP route exists and returns a response, but the response is hardcoded/canned, not computed |
| π Planned | Scaffolding only (a stub crate, an empty worker loop, a schema table) β no real logic yet |
Auth & identity β β Shipped
- Email/password registration and login,
bcrypt-hashed, Postgres-backed - GitHub OAuth and Google OAuth login, real
reqwestcalls to each provider's token + profile endpoints, upserts the user in Postgres - JWT access tokens (HMAC-SHA256 via
jsonwebtoken);GET /api/v1/auth/meis the one route currently gated by theAuthUserextractor
GitHub sync β π§ Engine built
- Repository and commit sync client (
crates/github), webhook HMAC-SHA256 signature verification β both real and tested, not yet called from an HTTP route (POST /api/v1/repositories/syncis documented indocs/API/but doesn't exist in the router yet)
Manifest / tech-stack parser β π§ Engine built
- Detects language, framework, database, cloud provider, CI system, and
architecture pattern straight from
Cargo.toml,package.json,Dockerfile,docker-compose.yml,requirements.txt, andREADME.mdβ real parsing logic incrates/parser, not yet wired to a route
ATS scoring β π§ͺ Mocked route, π§ real engine underneath
crates/ats/scorer.rsis a real, unit-tested 8-factor weighted scoring engine with a skill-taxonomy matcher;POST /api/v1/ats/scoreexists but currently always returns a flat92instead of calling it
Hybrid search β π§ Engine built
crates/search/hybrid_search.rsimplements real, tested Reciprocal Rank Fusion over keyword +pgvectorsimilarity;POST /api/v2/search/hybridexists but returns a static canned payload today
Resume / Portfolio / Analytics β π§ͺ Mocked routes
crates/resume(schema, export, templates),crates/portfolio(builder, theming, SEO),crates/analytics(activity/contribution scores, tech distribution, contribution heatmap) all exist as crates; their routes (resumes/generate,analytics/overview) currently return a fixed UUID / fixed numbers rather than computed results
AI provider abstraction β π§ͺ Mocked implementations
- A shared
AIProvidertrait with a fallback chain across OpenAI / Gemini / Claude / Ollama (crates/ai) β the abstraction, prompt builder, and fallback logic are real and tested, but every concrete provider currently returns a hand-formatted placeholder string instead of making a real API call.OPENAI_API_KEYis read from config already; the request code that would use it isn't written yet
Career / Jobs / Interview / Learning / Recommendation β π Planned
- Workspace crates exist (
crates/career,crates/jobs,crates/interview,crates/learning,crates/recommendation), most a single small file each; the/api/v2/*routes for these areas return static canned JSON
Background worker β π Planned
apps/workerspawns 8tokio::spawnloops (sync/github/resume/portfolio/ embedding/notification/cleanup/scheduler) that currently just log on a timer β thejob_queue/dead_letter_jobstables and the typed worker structs incrates/jobsexist in the schema/codebase but aren't connected to this process yet
CLI β π Planned
apps/cliis a two-line placeholder;ARCHITECTURE.mddescribes aclap-based admin CLI that hasn't been started
Today β what actually runs, end to end, in the deployed container:
flowchart LR
C["Client"] --> R["Axum Router<br/>apps/api/src/routes.rs"]
R --> H["Handlers<br/>apps/api/src/handlers"]
H -->|"sqlx query"| DB[("Postgres 16<br/>+ pgvector<br/>Supabase")]
H -->|"reqwest"| GH["GitHub / Google<br/>OAuth endpoints"]
H --> JWT["JWT issuance<br/>crates/auth"]
style DB fill:#eff6ff,stroke:#1d4ed8,color:#111827
Target β the intended flow once the worker and AI layer are wired up (from
the schema, ROADMAP.md, and the worker scaffolding already in place):
flowchart LR
API["apps/api handler"] -->|"INSERT"| Q[("job_queue table")]
W["apps/worker<br/>polls job_queue"] --> Q
W --> J["crates/jobs<br/>sync / embedding / resume /<br/>portfolio / notification / cleanup"]
J --> AI["crates/ai<br/>OpenAI Β· Gemini Β· Claude Β· Ollama<br/>with fallback chain"]
AI --> EMB[("pgvector embedding tables<br/>repository / resume / project /<br/>document / code / skill")]
EMB --> SEARCH["crates/search<br/>hybrid RRF fusion"]
style Q fill:#fef9c3,stroke:#ca8a04,color:#111827
style EMB fill:#eff6ff,stroke:#1d4ed8,color:#111827
~20 crates, workspace version 0.2.0, resolver = "2":
devresume-api/
βββ apps/
β βββ api/ Axum HTTP server β depends on every domain crate below
β βββ worker/ Standalone Tokio process β depends on common only (see Features)
β βββ cli/ Admin CLI β depends on common only (placeholder, see Features)
βββ shared/ Pagination + shared utility types
βββ crates/
β βββ common/ Config, DB pool, error types, shared models β everything else depends on this
β βββ auth/ JWT issuance, bcrypt hashing, GitHub/Google OAuth clients
β βββ github/ Repo/commit sync client, webhook signature verification
β βββ parser/ Manifest-based tech-stack + architecture-pattern detector
β βββ ai/ AIProvider trait, fallback chain, prompt engine
β βββ resume/ Resume schema, export, templates
β βββ portfolio/ Portfolio builder, theming, SEO
β βββ ats/ 8-factor ATS scoring engine + skill taxonomy
β βββ analytics/ Activity/contribution scoring, tech distribution, heatmap
β βββ search/ Hybrid (keyword + pgvector) search, RRF fusion
β βββ jobs/ Typed background-job worker definitions
β βββ notification/ Email/alert handlers (stub)
β βββ storage/ MinIO/S3 document client (stub)
β βββ career/ Career insights (stub)
β βββ interview/ Interview practice (stub)
β βββ learning/ Learning recommendations (stub)
β βββ recommendation/ General recommendations (stub)
βββ migrations/ SQLx migrations β 39 tables across 2 files (see below)
Path-versioned, both live in the same router/binary β there's no separate deployment per version.
| Route | Version | Status |
|---|---|---|
POST /auth/register, /auth/login |
v1 | β Shipped |
GET/POST /auth/github, /auth/github/callback |
v1 | β Shipped |
GET/POST /auth/google, /auth/google/callback |
v1 | β Shipped |
GET /auth/me |
v1 | β Shipped (only route with an auth guard today) |
POST /auth/logout, /auth/refresh |
v1 | π§ͺ Mocked (refresh returns a static status, no token verification yet) |
GET /repositories |
v1 | π§ͺ Mocked |
POST /resumes/generate |
v1 | π§ͺ Mocked (returns a UUID, not a generated resume) |
POST /ats/score |
v1 | π§ͺ Mocked (real scorer exists in crates/ats, not called yet) |
GET /analytics/overview |
v1 | π§ͺ Mocked |
POST /search/hybrid |
v2 | π§ͺ Mocked (real RRF engine exists in crates/search, not called yet) |
GET /career/insights |
v2 | π§ͺ Mocked |
/jobs/applications |
v2 | π§ͺ Mocked |
/interview/practice |
v2 | π§ͺ Mocked |
/recommendations |
v2 | π§ͺ Mocked |
GET /, /health, /health/live, /health/ready, /api/openapi.json |
β | β Shipped |
A wider surface (GET /resumes, POST /repositories/sync, portfolio publish
endpoints, β¦) is documented in docs/API/*.http as the intended target β treat
those as a spec to build toward, not routes that exist yet.
Auth pattern: JWT Bearer tokens, HMAC-SHA256. Almost every route above other
than auth/me is currently reachable without the bearer token being
checked β CORS is also fully open (Any/Any/Any) at this stage of
development. Both are pre-hardening defaults, not a production security
posture; see Roadmap.
Two migrations, 39 tables:
0001_initial_schema.sqlβ users/accounts/sessions, repositories, commits,repository_embeddings, projects, technologies, skills, resumes, portfolios,ats_reports,ai_jobs/ai_results, notifications,career_timeline, documents,activity_logs0002_enterprise_schema.sqlβ 5 morepgvectorembedding tables (resume/project/document/code/skill),job_queue/job_history/dead_letter_jobs, files/artifacts/exports,career_goals,job_applications,interview_sessions,recommendations
Full entity relationships are in docs/DOMAIN_MODEL.md.
# Postgres 16 with the pgvector extension must be reachable at DATABASE_URL
cp .env.example .env
cargo run --bin api # runs sqlx::migrate! automatically on boot, binds :8080
cargo run --bin worker # standalone process β currently just logs on a timer
cargo run --bin cli # currently a no-op placeholderEnv vars actually read by crates/common/src/config.rs today (the rest of
.env.example is forward-looking config for not-yet-wired features):
| Variable | Purpose |
|---|---|
PORT, ENVIRONMENT |
Server bind port, environment name |
DATABASE_URL |
Postgres connection string (pgvector extension required) |
REDIS_URL |
Parsed at startup; no handler uses it yet |
JWT_SECRET |
HMAC signing key for access tokens |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET / GITHUB_CALLBACK_URL |
GitHub OAuth |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / GOOGLE_REDIRECT_URI |
Google OAuth |
OPENAI_API_KEY |
Read into config; not yet called by crates/ai's mocked provider |
WEB_URL |
Used to build OAuth redirect URLs β not in .env.example yet, add it locally |
cargo test --workspace # unit tests β real coverage on ats/, search/, github webhooks, ai prompt engine
cargo fmt --check
cargo clippy --workspaceflowchart LR
PR["push to main<br/>(paths-ignore **.md)"] --> CI["GitHub Actions<br/>azure-deploy.yml"]
CI -->|"build (2-stage Dockerfile)"| IMG["Image"]
IMG -->|push| GHCR[("ghcr.io/.../devresume-api")]
CI -->|"OIDC login, no stored secret"| AZ["az containerapp update"]
GHCR --> AZ
AZ --> ACA["Azure Container Apps<br/>min-replicas 0"]
ACA -->|"Supavisor pooler :6543<br/>sslmode=require"| SB[("Supabase Postgres<br/>+ pgvector")]
style SB fill:#eff6ff,stroke:#1d4ed8,color:#111827
- Runtime: Azure Container Apps, Consumption plan,
min-replicas 0(cold starts of 20β40s+ after idle), sharing a pre-existing Container Apps environment since the Azure subscription caps at one environment. - Database: Supabase Postgres via the Supavisor transaction pooler
(port
6543,sslmode=require) β the direct host is IPv6-only and unreachable from Container Apps. Migrated off Azure Postgres Flexible Server for cost. - Registry: GHCR, pushed with the workflow's built-in
GITHUB_TOKEN. - CI (
ci.yml) βcargo fmt --check, clippy, build, and test on every push todevelop/feature/**/fix/**/refactor/**and PRs intomain/develop. - CD (
azure-deploy.yml) β on push tomain, builds and pushes the image, authenticates to Azure via OIDC (no stored cloud credential), thenaz containerapp update. Migrations run automatically at container boot viasqlx::migrate!β there's no separate migration-gate step. - Secrets are created as Container Apps secrets and referenced via
secretref:<name>, never passed as plain env vars; rotating one requires an explicit revision restart. - Only
apps/apiis containerized and deployed today βworkerandcliaren't part of the image yet.
Full provisioning steps, region constraints, and the one-time setup script are
in AZURE_DEPLOYMENT.md.
Full detail in docs/ADR/; one line each:
- ADR 0001 β Axum over Actix-web, for async/Tokio-ecosystem fit and low overhead.
- ADR 0002 β SQLx over Diesel, for async and compile-time-checked SQL with easy access to raw Postgres features (CTEs, vector ops,
tsvector). - ADR 0003 β
pgvectorover a dedicated vector database, to keep embeddings co-located with relational data. - ADR 0004 β Modular monolith over microservices for early-stage velocity, with crate boundaries as the seam for a future split.
- ADR 0005 β AI providers abstracted behind one shared trait specifically to avoid vendor lock-in across OpenAI/Gemini/Claude/Ollama (the trait is real today; the concrete providers are still mocked β see Features).
- ADR 0006 β Background jobs on a Postgres-backed queue rather than Redis+Sidekiq-style, for durability across restarts without extra infra.
Ten milestones, from docs/ROADMAP.md β a narrative order
to build in, not a "done" checklist:
Auth β GitHub sync β Parser β AI engine β Resume generation β
Portfolio β ATS + Analytics β Hybrid search β Notifications + Workers
β Production hardening
Concretely, the next real steps out of today's state are: call the existing
ats/search engines from their already-defined routes instead of returning
mocked JSON, replace the mocked AIProvider implementations with real API
calls, connect apps/worker to job_queue instead of its timer loops, and add
an auth-guard + rate-limiting middleware layer before any of this is exposed
beyond development.
Reflects the code as it stands today, not the aspirational posture described
in docs/SECURITY.md (which currently describes controls β Argon2id hashing,
rate limiting, CORS allowlisting β that aren't implemented yet):
- Passwords are hashed with
bcrypt(not Argon2id, despite whatdocs/SECURITY.mdcurrently says). - Access tokens are JWTs signed with HMAC-SHA256; only
GET /auth/meis currently gated behind the auth extractor β every other route is reachable without a valid token at this stage. - CORS is fully open (
Anyorigin/method/header) β appropriate for active development, not for a public production rollout. - OAuth secrets and the database connection string are read from environment variables locally and from Container Apps secrets in production; nothing is hardcoded or committed.
- GitHub webhook signatures are verified with HMAC-SHA256 in
crates/github(real, tested) β even though no route currently receives webhook traffic.
ChamathDilshanC (dilshancolonne123@gmail.com)