EsenceLab
Multi-service product system: TypeScript API with authentication and RBAC, Python AI service, PostgreSQL.
- TypeScript
- Express
- Next.js
- Python FastAPI
- Supabase PostgreSQL
- Docker
- RBAC suite
- CI
- source
Problem
Students can't map resumes to skill gaps, recruiters screen manually, admins lack one control surface. The engineering problem underneath: three deployables (web, API, AI service) share one data model, and the boundaries hold auth, role-gated onboarding, resume parsing, and job matching.
Constraints
- One meaningful AI workflow (resume parse + match); secondary LMS/coach features stay out of the story.
- Managed Postgres (Supabase) for operations; authorization enforced in the API, not delegated to permissive RLS.
- AI calls must degrade gracefully: deterministic matching first, LLM optional.
Architecture
Frontend (Next.js) ↓ API (Express: JWT + RBAC, rate limits, request IDs) ↓ PostgreSQL (Supabase: users, resumes, candidates, jobs, applications) ↓ AI service (FastAPI: /ai/parse-resume, /ai/match)
Service calls are synchronous fetch with an internal service token and 8–12s timeouts, no queues yet, documented as the next step. Matching is deterministic first (overlap×0.55 + TF-IDF×0.45 with fallbacks); the LLM path is optional with local fallback.
Implementation
Auth with JWT plus role-based access across student/recruiter/admin, including a gated recruiter flow (request → admin approve → temp password → login). Resume upload (PDF, 4MB cap) with structured parsing. Jobs CRUD plus applications, skill-overlap matching with explanations. Per-endpoint in-memory metrics and /api/health; Docker per service; CI with RBAC smoke and stress suites running against a mocked Supabase.
Engineering decisions
- Supabase as managed PostgreSQL; RLS policies are permissive by configuration and documented as not relied upon; authorization lives in Express.
- Deterministic matching before LLM: explainable, free, testable. The model is an add-on, not the foundation.
- Monolith split by extraction, pure functions first: role helpers moved to
backend/src/auth/roles.tswith zero behavior change, verified by the green RBAC suite before the next step.
Challenges
The API grew into a ~6K-line single file that still passes its tests, the classic "works but can't evolve" state. The fix is mechanical, not clever: extract dependency-free modules first (done for auth roles), convert closures to arguments for the rest, and keep the RBAC suite green at every step. The split plan with verification gates lives in docs/ARCHITECTURE.md.
Benchmarks
RBAC smoke + stress suites green (SUPABASE_MOCK=1). Latency thresholds exist (slow-endpoint 1200ms, p95 alert 2500ms) but are alerts, not measurements, so no latency numbers are cited.
Trade-offs
- Synchronous AI calls instead of a queue: simpler, with timeouts. A queue arrives when AI latency actually blocks requests.
- In-memory monitoring instead of external APM: documented gap.
- Secondary features (courses, interviews, analytics) exist in the codebase but are deliberately excluded from this story.
Lessons
Product engineering is boundary work: API contracts, auth semantics, data ownership, and knowing which features to leave out of the narrative. A monolith you can split step by step beats a microservice diagram you can't deploy.