Перейти к содержанию

Architecture

A monolith of two parts in one repository: frontend/ and backend/. One .env, one CI. There is no shared JavaScript between them.

flowchart LR
  browser[React SPA] --> api["FastAPI /api/v1"]
  api --> pg[(PostgreSQL)]
  api --> es[Elasticsearch]
  api --> mail[Resend]
  api --> llm[RouterAI]

Candidate search uses Elasticsearch when ELASTICSEARCH_URL is set, otherwise the database. Mail is Resend. Without a key outside production the confirmation code is written to the log. Resume recognition and the AI interview use RouterAI only when ROUTERAI_API_KEY is set.

Frontend

Feature-Sliced layers: app → pages → widgets → features → entities → shared. A layer does not import a layer above it. A slice's public API is index.ts. Entities refer to each other through @x.

Routes are in frontend/src/shared/config/routes.ts. Among them: /, /auth, cabinets /profile/*, the feed /feed, /vacancies, /responses, /calendar, /matching, /testing, /testing/builder, /admin, public /companies/:id and /candidates/:id, and the FSP return /profile/fsp/callback.

Server state is TanStack Query. The session is an external store. Client access is RequireAuth. The real check is on the backend.

Backend

Layer What it does
api/routes Paths, statuses, dependencies. No domain rules
dependencies DB session, current user, require_roles
services Scenarios
repositories Queries
models SQLAlchemy 2, not returned outside
schemas Pydantic, camelCase on the wire
core Settings, Argon2id, JWT, errors
admin A separate admin package: bank, analytics, ban list, parameters, verification
integrations/fsp The FSP ID provider and the registry client

An API error is {"error": {"code", "message", "details"}}. An unfinished scenario answers 501 with code not_implemented, without fake logic.

Passwords are Argon2id. An unknown email and a wrong password return one answer. A dummy hash is verified to keep the timing even. JWT is HS256, with claims close to Keycloak (sub, email, realm_access.roles), so RS256 via JWKS can be accepted later. The minimum password length in the registration schema is 8 characters (PASSWORD_MIN_LENGTH in schemas/auth.py). That is shorter than NFR-04.