What this app is, who uses it, and the workflows it supports. Implementation lives in src/app/modules/; backend logic and persistence are in the sibling web-api/ repository — this doc describes only what the frontend mediates.
The product is techinterview.space — a Russian-language platform aimed at IT workers in Kazakhstan and the surrounding region. The frontend mediates four user-visible feature areas plus an admin console.
The audience and tone of the site are reflected in the static meta tags (src/index.html) and the default meta-tag content set by MetaTagService.returnDefaultMetaTags() in src/app/services/meta-tags.service.ts: salary statistics and company reviews in IT, in Russian.
Each area is a lazy-loaded NgModule under src/app/modules/<area>/, with its leaf components in <area>/components/ and its routes in <area>-routing.module.ts.
User-submitted salary surveys aggregated into charts and statistics.
Module: src/app/modules/salaries/. Top-level service: src/app/services/user-salaries.service.ts. Survey form service: salaries-survey.service.ts. Historical data: historical-charts.service.ts.
Two coexisting entry points:
/salaries— interactive Chart.js dashboards, requires the user to have submitted a salary themselves (gating done backend-side)./salaries/overview— public, server-rendered, plain-HTML alternative for crawlers and AI bots. Same chart data exposed via?allowReadonly=true. See geo.md for why this exists.
User-written reviews of employers, with rating, ratings breakdown, and moderation.
Module: src/app/modules/companies/. Top-level service: companies.service.ts. Resolvers in companies/resolvers/ fetch company data before route activation so SSR meta tags can be set in time.
Public pages (/companies, /companies/:id, /companies/recent-reviews) are server-rendered. MetaTagService.setCompanyMetaTags() writes per-company og:*, twitter:*, and canonical URL on each request.
Job vacancies published by authenticated users. A company link is optional: the form takes a company picker (companyId), a free-text display name (companyName, e.g. "NDA"), and a "hide attached company" toggle (hideAttachedCompany) — at least one of company/name is required, and hiding needs a linked company. The list/detail bind companyName (the backend's effective name: free text, else the linked company's name, suppressed when hidden); the edit form pre-fills from the raw companyNameText. Statuses: Draft / Public / Closed, plus soft delete.
Module: src/app/modules/vacancies/. Service: vacancies.service.ts. Resolver in vacancies/resolvers/ fetches the vacancy before route activation for SSR meta tags. Pages: public list (/vacancies) and detail (/vacancies/:id) are server-rendered; create (/vacancies/new), edit (/vacancies/:id/edit), and "My vacancies" (/vacancies/my) are authenticated SPA, guarded by AuthGuard. Descriptions are Markdown, rendered with ngx-markdown under SecurityContext.HTML sanitization (user-generated content). The detail page re-fetches client-side with auth when the anonymous SSR resolver returns nothing, so owners/admins can view their non-public vacancies. Ownership (only the creator edits) is enforced by the backend; the UI shows the edit affordance when currentUser.id === vacancy.authorId.
Interview templates (question banks) and recorded interview sessions.
Module: src/app/modules/interviews/. Services: interview-templates.service.ts, interviews.service.ts. Public listing at /interviews/templates/public is server-rendered for SEO; everything else is authenticated SPA.
Public, slug-addressable survey pages. Authors create surveys; respondents submit replies anonymously.
Module: src/app/modules/surveys/. Service: public-surveys.service.ts. Slug pages (/surveys/:slug) are server-rendered for social-share previews; /surveys/new and /surveys/my-surveys are SPA.
Public user profiles and authenticated account self-service.
Module: src/app/modules/users/. Account-relevant pieces under the home module's me/ component (account dashboard) and unsubscribe-me/ (one-click email-unsubscribe via tokenised URL).
Composite landing module that also owns auth-adjacent and standalone pages: login, registration, password reset, email verification, OAuth callbacks, privacy policy, about pages, the design-system showcase, and the various error views (not-found, not-authorized, no-permission, server-unavailable).
Module: src/app/modules/home/.
Internal console for moderation, content management, and integrations.
Module: src/app/modules/admin/, gated by AuthGuard + AdminGuard at the root router. Areas inside admin:
- Users, companies, company-review approval, vacancy moderation (
/admin/vacancies— read-only fields; status move to Draft/Closed and restore), surveys - Salary admin, "not-in-stats" salaries, sourced/imported salaries, historical-data templates
- Interview-template moderation
- OpenAI prompt management
- Telegram integrations: user settings, bot configurations, stat-data subscriptions, review-stat subscriptions, job-posting subscriptions
- GitHub integrations: profiles, chats, processing jobs
- M2M (machine-to-machine) clients: list, create, detail
- Channel stats, label entities (skills, work industries, professions)
- Tools: background jobs, currencies, QR-code generation, send-email
The exact route table is in src/app/modules/admin/admin-routing.module.ts — keep it as the source of truth, not this doc.
ApplicationUser (src/app/models/application-user.ts) is the canonical user shape returned from GET /api/account/me. It is wrapped in ApplicationUserExtended (src/app/models/extended/) which adds derived helpers (e.g., hasRole(UserRole)).
UserRole is defined in src/app/models/enums/. Role-gated behaviour goes through RoleGuardBase (route-level) or the appHasRole directive (template-level).
For the auth flow itself, see authentication.md.
environment.isUnderMaintenance flips the root router (src/app/app-routing.module.ts) so every URL redirects to /maintenance. To enable, set the flag in the relevant environment*.ts and rebuild.
Login and registration forms send two anti-bot signals to the backend:
website— a honeypot input hidden via the global.form-field-extraclass. Real users never see it; bots auto-fill it.formDurationSeconds— elapsed time since the form mounted, recorded inngOnInit(browser only — guarded withisPlatformBrowserbecauseDate.now()is measured for human-vs-bot heuristics).
See LoginPageComponent and RegisterPageComponent in src/app/modules/home/components/.
The app is Russian-facing. UI strings in components and meta tags are in Russian; identifier names, code comments, and these docs are in English. When adding user-facing copy, match the surrounding component.
src/index.html carries baseline og:*, twitter:*, and JSON-LD blocks. Page-specific overrides come from MetaTagService (run during SSR via resolvers) and JsonLdService. Server-rendered routes are the only ones that benefit; for SPA routes the static fallback in index.html is what crawlers see.
For the full SEO/GEO strategy, see geo.md.