База знаний — продукт и доменная модель Qadam
Обновлён 1 апр. 2026 г., 12:41 · 0 комментариев
База знаний — продукт и доменная модель Qadam
Паспорт документа
- Статус документа: working reference
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: при изменении инженерного backlog, локального workflow или платформенного статуса
- Область применения: внутренний инженерный knowledge/rules/backlog слой проекта
- Связанные документы:
Этот файл содержит доменную и продуктовую базу знаний по Qadam. Для правил разработки и operational-практик используй rules/, а для текущего статуса платформы и production truth сверяйся с ../current-state.md, ../roadmap.md и ../api-routes.md.
Product Overview
Qadam (Қадам, "Step") is an EdTech aggregator for supplementary education in Uzbekistan. It connects learners (primarily parents looking for education for their children) with educational providers across three categories:
- Offline Learning Centers (
school_offline) — physical institutions: language schools, IT academies, exam prep centers, art studios, sports clubs - Online Schools (
online_school) — digital courses and mini-group classes sold on the platform - Individual Tutors (
individual_contributor) — private tutors, coaches, mentors
The core user journey: Parent searches → Finds a course → Submits a lead (inquiry) → Provider contacts them → Enrollment.
Product Name & Branding
- Product name: Qadam (Қадам)
- Primary color:
#1DB57A(green) - Font: "Outfit" (Google Fonts) + system sans-serif fallback
- Container: max-width
1280px - Responsive: mobile-first with
md/lgbreakpoints - Dark theme: not implemented
User Roles & Subtypes
Current Roles
| Role | Code | Subtypes | Description |
|---|---|---|---|
| Buyer | buyer | parent, student | Searches courses, submits leads, writes reviews |
| Seller | seller | school_offline, online_school, individual_contributor | Manages courses, handles leads, manages staff |
| Seller Staff | seller_staff | — | Employee bound to a seller organization |
| Admin | admin | — | Moderation, reference data, analytics |
Route Access
| Role | Routes | Key Actions |
|---|---|---|
| Guest | /, /item/*, /sellers/* | Browse catalog, view items, submit leads |
| Buyer | + /me/* | Manage leads, write reviews, edit profile |
| Seller | + /seller/* | Manage items, view leads, manage staff |
| Seller Staff | отдельного стабильного кабинета пока нет | В текущем production редиректится в staff-unavailable |
| Admin | + /admin/* | Moderation, reference data management |
Planned CRM Roles (Seller Dashboard)
| Role | Code | Access Level |
|---|---|---|
| Owner | owner | Full access including billing and org deletion |
| Admin | admin | Schedule, staff, clients, analytics management |
| Manager | manager | Leads, clients, bookings |
| Teacher | teacher | Own schedule, own groups, attendance marking |
CRM Permissions Matrix:
| Function | Owner | Admin | Manager | Teacher |
|---|---|---|---|---|
| Org settings | + | + | - | - |
| Role/staff management | + | + | - | - |
| Service management | + | + | + | - |
| Schedule (all staff) | + | + | + | - |
| Schedule (own) | + | + | + | + |
| Client base | + | + | + | Own only |
| Bookings (all) | + | + | + | - |
| Bookings (own) | + | + | + | + |
| Groups (all) | + | + | + | - |
| Groups (own) | + | + | + | + |
| Leads | + | + | + | - |
| Analytics (full) | + | + | - | - |
| Analytics (own) | + | + | + | + |
| Special offers | + | + | + | - |
| Finance/reports | + | + | - | - |
| Attendance marking | + | + | + | + |
Core Domain Entities
Item (Course/Service) — Central Entity
The Item is the universal entity representing any educational offering. It is the most important entity in the system.
Key attributes:
| Field | Type | Description |
|---|---|---|
item_id | UUID | Primary key |
seller_id | UUID FK | Owner seller |
school_id | UUID FK (nullable) | Linked offline school |
online_school_id | UUID FK (nullable) | Linked online school |
item_name | string | Course title |
item_desc | text | Full description |
item_shortdesc | text | Short description (for cards) |
item_price_from | decimal | Min price |
item_price_to | decimal | Max price |
item_age_group | string | Target age group label |
item_age_from / item_age_to | int | Age range |
item_classes_from / item_classes_to | int | School grade range |
subject_id | UUID FK | Subject (e.g., Mathematics) |
subject_group_id | UUID FK | Subject group (e.g., School subjects) |
item_studytype | enum | group, mini_group, one_on_one |
item_studyformat | enum | online, offline, hybrid |
item_language | enum | ru, uz_latin, uz_cyrillic, en, kk, tg, any |
item_studyduration_from / to | int | Duration range |
item_isvisible | boolean | Visibility flag |
item_image_url | string | Cover image |
location_id | UUID FK | Location reference |
schedule_id | UUID FK | Schedule reference |
specialoffer_group_id | UUID | Special offers group |
item_perfomer_group_id | UUID FK | Assigned teachers/performers |
moderation_status_id | UUID FK | Moderation status |
Buyer
A technical entity linking an account to either a Parent or Student profile:
buyer_type:parentorstudent- Parents can add students (children) to their profile
- Students can add parents to their profile
- This bidirectional linking is stored in the respective profile
Seller
A technical entity linking an account to one of three subtypes:
school_offline→SchoolProfile(name, phone, email, desc, location)online_school→OnlineSchoolProfile(name, phone, email, desc)individual_contributor→IndividualContributorProfile(name, phone, email, desc, location)
Lead (Inquiry)
A user inquiry about an item:
| Field | Description |
|---|---|
lead_type | trial (trial lesson) or buy (enrollment) |
lead_name | Contact name |
lead_phone | Contact phone |
lead_email | Contact email |
lead_comment | Free-text comment |
lead_source | Where the lead came from |
lead_status | created → contacted → enrolled or rejected |
is_contacts_confirmed | Phone/email verified |
Review
- Rating: 1-5 stars (integer)
- Text comment
- Linked to item, buyer, and seller
- Aggregated stats pre-calculated in SAL layer (
rating_avg,reviews_count)
Schedule & Performers
ScheduleProfile: datetime range, status, timezonePerformerProfile: active flag, description, specialization, experience, linked to seller_staffPerformerGroupProfile: groups performers for an item
Moderation
- Items go through moderation before publishing
- Status codes:
active,rejected,pending - Admin can add reason and comment
is_visible_to_sellercontrols whether rejection details are shown
Reference Data (Registries)
| Registry | Fields | Notes |
|---|---|---|
| Subjects | name, desc, group_id, group_name | Two-level hierarchy: group → subject |
| Locations | name, longitude, latitude | Cities/districts with coordinates |
| Tags | type, raw_value, name_ru, name_uz, is_visible | Bilingual tags |
| Events | name, type | User action types for analytics |
| Moderation Statuses | code, name | active, rejected, pending |
Canonical Enums & Status Machines
Source of truth. All specs MUST use these exact values. If a spec disagrees with this section, the spec is wrong — fix the spec.
ItemStatus (moderation_status)
enum ItemStatus {
draft // создан, не отправлен на модерацию
pending // на модерации у Admin
active // одобрен, публично виден (НЕ approved — это устаревшее)
rejected // отклонён, продавец видит причину
revision_required // требует доработки от продавца
}
Видимость в каталоге:
item_isvisible = true AND moderation_status = active
LeadStatus
enum LeadStatus {
new // только что создан (НЕ pending, НЕ created)
contacted // продавец связался с байером (НЕ processing)
enrolled // клиент записан на занятие
attended // пришёл на пробное занятие
no_show // не пришёл на пробное занятие
purchased // оплатил / оформил покупку
not_purchased // решил не покупать
rejected // продавец отклонил лид
}
ReviewStatus
enum ReviewStatus {
pending // ожидает первичной модерации
published // одобрен и виден публично (НЕ active)
rejected // отклонён модератором
pending_moderation // был published, получил жалобу → скрыт, ждёт повторной проверки
}
rating_avgсчитается только поstatus = published.
PriceType
enum PriceType {
per_lesson // цена за одно занятие
per_month // цена в месяц
per_package // цена за пакет N занятий (НЕ просто package)
one_time // разовая оплата за весь курс (НЕ subscription)
}
DiscountType
enum DiscountType {
percent // скидка в процентах
fixed_amount // скидка фиксированной суммой
gift // подарок / бонусное занятие
}
ItemView (аналитика просмотров)
ItemView — входит в MVP (Spec 11). Трекинг уникальных просмотров айтемов для seller dashboard. Spec 06 НЕ исключает ItemView — она исключает лишь view_count на публичной карточке айтема (не отображаем счётчик байеру). Трекинг пишется в БД, данные видны только продавцу в дашборде.
CPL Deduplication Rule
30-дневное окно: если за последние 30 дней уже существует лид с тем же buyer_account_id + item_id (или тем же lead_phone + item_id для незалогиненных) — CPL НЕ начисляется. Лид создаётся, но CplTransaction не генерируется. Это правило обязательно учитывается в Spec 16 (CPL Billing) при биллинге.
Category Tree
The platform has 11 top-level categories with subcategories:
| # | Category | Slug | Subcategories |
|---|---|---|---|
| 1 | School & University subjects | school | Math, Physics, Chemistry, Biology, History, Informatics, Russian, Literature, Social Studies, Uzbek |
| 2 | Foreign languages | languages | English, German, Chinese, French, Russian as foreign, Uzbek as foreign |
| 3 | IT & Business | it | Programming, Robotics, 3D Modeling, Circuit Design |
| 4 | Creative arts | creative | Drawing, Music, Dance, Theater, Design |
| 5 | Beauty & Health | beauty-health | — |
| 6 | Sports | sports | Football, Swimming, General fitness |
| 7 | Driving | driving | — |
| 8 | Soft skills | soft-skills | Critical thinking, Public speaking |
| 9 | Financial literacy | finance | — |
| 10 | Chess | chess | — |
| 11 | Other | other | — |
Categories use a slug-based system. The getCategoriesInfo(slug) function resolves a slug to its display labels (parent + child).
Пользовательские сценарии
Реально работающие сейчас
1. Catalog → Item → Lead
Buyer открывает каталог →
Открывает карточку айтема →
Создаёт лид через buyer-авторизацию →
Лид появляется в seller-кабинете
2. Seller registration → Profile → Moderation
Seller проходит регистрацию →
Создаёт и редактирует профиль →
Создаёт айтем →
Отправляет его на модерацию →
После approve айтем попадает в публичный каталог
3. Buyer registration / profile backend flow
Buyer регистрируется через registration API →
Создаёт или обновляет профиль →
Parent управляет детьми →
Student управляет interests
Частично реализовано
- Публичный seller profile
- Reviews flow
- Admin moderation
- Buyer cabinet
Осознанно не считать реализованным
- Quiz flow
- Telegram lead notifications
- CRM booking / schedule / groups
- Owner analytics и promo tools
Journey 10 (CRM): Teacher Role
Login → Limited menu: My Schedule, My Groups, My Students →
Calendar → Only own bookings → Group → Mark attendance →
Own stats: lessons count, workload percentage
Pages & UI Architecture
Public Pages
| Page | URL | Key Elements |
|---|---|---|
| Home / Catalog | / | Search bar, category nav (pill buttons), filter sidebar (desktop) / drawer (mobile), course feed с infinite scroll, quiz modal, lead modal |
| Course Detail | /item/[slug] | SSR page, course details, similar courses, reviews block, sidebar with price + "Enroll" button, lead modal |
| Seller Profile | /sellers/[id] | Seller info, course list, rating & reviews |
| Login | /login | Role selection (Buyer/Seller), phone/email login, multi-account support |
| Registration | /register | 4-step flow: role → account → subtype → onboarding |
Buyer Dashboard (/me/*)
Navigation: Overview | Profile | My Leads | My Reviews
| Page | URL | Content |
|---|---|---|
| Overview | /me | Dashboard cards with quick links |
| Profile | /me/profile | Edit name, phone, email |
| My Leads | /me/leads | Lead list + status badges (New, Contacted, Enrolled, Rejected) |
| My Reviews | /me/reviews | Review list with ratings |
Seller Dashboard (/seller/*)
Navigation: Overview | About School | My Courses | Leads | Staff
| Page | URL | Content |
|---|---|---|
| Overview | /seller | Stats: course count, leads, rating |
| About School | /seller/profile | Edit organization profile |
| My Courses | /seller/items | Course list + moderation status badges |
| Create Course | /seller/items/new | Multi-step creation form |
| Edit Course | /seller/items/[id]/edit | Pre-filled edit form |
| Leads | /seller/leads | Incoming leads + status change |
| Staff | /seller/staff | CRUD staff (name, phone, email, role) |
Admin Panel (/admin/*)
Navigation: Overview | Moderation | All Leads | Subjects | Locations
| Page | URL | Content |
|---|---|---|
| Overview | /admin | Stats: pending, total courses, leads, sellers |
| Moderation | /admin/moderation/items | Items pending review + Approve/Reject |
| All Leads | /admin/leads | Table view of all system leads |
| Subjects | /admin/reference/subjects | CRUD subject groups and subjects |
| Locations | /admin/reference/locations | CRUD locations (City, District, Region) |
UI Components Inventory
| Component | Purpose |
|---|---|
Header | Global nav: logo, city selector, language switcher (RU/UZ), auth buttons |
CategoryNav | Category/subcategory navigation (pill-style buttons) |
SearchBar | Search input + location selector + mobile filter toggle |
FilterSidebar | Desktop filter panel (checkboxes, sliders for price/age) |
FilterDrawer | Mobile filter modal (same filters as sidebar) |
HeroBanner | Hero banner on home page |
CourseFeed | Course card grid + infinite scroll через sentinel |
CourseCard | Card: photo, title, seller name, rating, price, format badge, location |
CourseCardSkeleton | Loading skeleton for course cards |
SimilarItems | "Similar courses" block on item detail page |
ReviewsBlock | Reviews list with ratings on item detail page |
ReviewForm | Star rating + text input for writing reviews |
LeadModal | Inquiry modal: name, phone, type (trial/buy), comment |
QuizModal | Quiz dialog for personalized recommendations |
OfferSidebar | Course detail sidebar: price, schedule summary, "Enroll" button |
QadamMap | OpenStreetMap map with markers |
NominatimAutocomplete | Address autocomplete with debounce |
ItemBadge | Status/tag badge |
Catalog Filter Fields
The catalog supports filtering by:
| Filter | Type | Values |
|---|---|---|
| Format | checkbox | online, offline, hybrid |
| Language | checkbox | ru, uz_latin, uz_cyrillic, en, kk, tg, any |
| Study type | checkbox | group, mini_group, one_on_one |
| Time slot | checkbox | morning, afternoon, evening |
| Price range | slider | priceFrom — priceTo |
| Age range | slider | ageMin — ageMax |
| Category | pill buttons | From CATEGORY_TREE |
| Location | selector | From LocationRegistry |
Filters applied via URL query params with AND logic. UI-driven pagination works through infinite scroll; page и limit используются как внутренние transport-параметры запросов, а не как пользовательский URL state.
When Working with the Catalog
The catalog is the main public-facing feature:
- Displays items with filters (subject, location, type, price range)
- Uses server-side rendering for SEO
- Catalog data is cached in Redis with 5-minute TTL
- Search is powered by PostgreSQL full-text search (with plans for Elasticsearch later)
- Categories are displayed in a horizontal navigation bar (pill buttons)
Filtering Logic
Filters are applied via URL query parameters:
?subject=mathematics&location=tashkent&studyFormat=OFFLINE- Filters are combined with AND logic
- Pagination: infinite scroll + transport-level
page/limitпод капотом
When Working with Locations and Maps
Locations are a critical part of the platform — learning centers, tutors, and group classes have physical addresses. Detailed rules in rules/data-locations-and-maps.md.
Location Types
| Type | Has Address | Has Coordinates | Example |
|---|---|---|---|
OFFLINE_ADDRESS | Required | Required | Language school at a fixed address |
ONLINE | No | No | Online-only course |
HYBRID | Required | Required | School with online option |
MOBILE_TUTOR | Coverage area only | No | Tutor visits student's home |
MULTIPLE_BRANCHES | Multiple via ItemBranch | Yes, per branch | Large school chain |
Geocoding — Nominatim (OpenStreetMap)
We use Nominatim for address autocomplete and geocoding:
- Component:
NominatimAutocomplete— debounced autocomplete with 300ms delay - Map:
QadamMap— renders OpenStreetMap tiles with markers - API:
https://nominatim.openstreetmap.org/searchwithcountrycodes=uz - User-Agent: Required by Nominatim policy — set
Qadam/1.0 (contact@qadam.uz) - Rate limit: Max 1 request per second (Nominatim policy)
- Response language:
accept-language: uz,ru
Uzbekistan Coordinate Bounds
All coordinates must fall within Uzbekistan's bounding box:
- Latitude: 37.0 — 45.6
- Longitude: 55.9 — 73.2
Any coordinates outside these bounds are rejected. This prevents wrong map rendering (e.g., pin in the Atlantic Ocean at [0, 0]).
Key Cities and Their Approximate Coordinates
| City | Latitude | Longitude |
|---|---|---|
| Tashkent (Тошкент) | 41.2995 | 69.2401 |
| Samarkand (Самарқанд) | 39.6542 | 66.9597 |
| Bukhara (Бухоро) | 39.7747 | 64.4286 |
| Namangan (Наманган) | 41.0011 | 71.6722 |
| Andijan (Андижон) | 40.7830 | 72.3442 |
| Fergana (Фарғона) | 40.3842 | 71.7890 |
| Nukus (Нукус) | 42.4628 | 59.6035 |
| Karshi (Қарши) | 38.8610 | 65.7986 |
Common Location Bugs to Watch For
- Map at
[0, 0]: Coordinates arenull/undefined, map library defaults to zero. Always check before rendering. - Swapped lat/lon: Nominatim returns
lat,lonbut GeoJSON uses[lon, lat]. Always use typedCoordinatesobject. - Results from wrong country: Missing
countrycodes=uzin Nominatim query. - SSR crash on map: Map libraries use
windowobject. Always lazy-load withssr: false. - Rate limited: No debounce on autocomplete. Always debounce 300ms minimum.
- Private address exposed: Missing
displayPubliclycheck in DTO. Always filter.
Privacy Rules for Locations
- City name: Always public (needed for search/filtering)
- Full address: Only if
displayPublicly === true - Coordinates: Only if
displayPublicly === true - Map rendering: Only on item detail page and only if address is public
When Working with Authentication
- JWT-based authentication via NestJS
- Tokens stored in httpOnly cookies (not localStorage — this is a fix from the old project which used localStorage)
- Access token (short-lived) + Refresh token (long-lived)
- Auth flow: Register → Verify Phone → Login
- Phone verification via SMS (Uzbekistan phone numbers: +998)
- Multi-account support: users can be both buyer and seller
- Login supports both phone and email
- Registration is a 4-step flow: role → account data → subtype → onboarding
- Global JWT guard with
@Public()decorator for public endpoints
When Working with Internationalization
Detailed rules in rules/patterns-i18n.md.
The platform supports three languages:
- uz (Uzbek) — primary/source language, always complete
- ru (Russian) — secondary language (majority of current users)
- en (English) — tertiary language
Configuration Architecture
| Layer | File | Purpose |
|---|---|---|
| Config source of truth | /i18n.json | Defines source (uz) and target (ru, en) locales |
| Next.js i18n config | apps/web/i18n.config.ts | Reads from i18n.json, configures next-intl |
| Translation files | apps/web/messages/{uz,ru,en}.json | One JSON file per locale |
| Server-side loading | apps/web/lib/i18n-server.ts | loadTranslations() with caching and Uzbek fallback |
| Locale detection | apps/web/lib/get-locale.ts | Cookie → Accept-Language → default (uz) |
| Server Components | getTranslations() from next-intl/server | No JS bundle impact |
| Client Components | useTranslations() from next-intl | For interactive elements only |
Two Types of Text
- UI strings (buttons, labels, error messages) → Translation JSON files, always translated to all 3 languages
- Content (item titles, descriptions, reviews) → Database, in seller's/user's chosen language, NOT translated via i18n
Fallback Strategy
Missing translation keys fall back to Uzbek text (source language). This is implemented by merging locale translations on top of the complete Uzbek file:
const merged = { ...uzTranslations, ...localeTranslations };
This ensures users never see raw translation keys like catalog.title — they see Uzbek text at minimum.
Pluralization
Uzbek and Russian have different plural rules. Always use ICU message format:
{
"resultsCount": "{count, plural, one {# kurs topildi} other {# ta kurs topildi}}"
}
When Adding New UI Text
- Add the key to
messages/uz.jsonfirst (source language) - Add translations to
messages/ru.jsonandmessages/en.json - Use
t('namespace.key')in the component - Prefer Server Component
getTranslations()over Client ComponentuseTranslations()
Analytical Database Architecture
The database follows a three-layer analytical architecture designed for event sourcing and data warehousing. This architecture is critical for analytics and must be preserved.
Layer Overview
SAA (Staging Area Applied) → SAL (Staging Area Load) → DDS (Data Vault)
| Layer | Purpose | Mutability |
|---|---|---|
| SAA | Enriched operational data. Append-only event streams and profile snapshots | Immutable — new values = new row |
| SAL | Pre-calculated aggregates and transformations | Immutable — recalculated, not updated |
| DDS | Core data warehouse using Data Vault (Hub/Link/Satellite) | Immutable — deduplicated before insert |
Key Principles
- Append-only: Rows in SAA, SAL, and DDS are never modified. If a value changes, a new row is written with a new
event_time. - Every row has
event_time: This is the timestamp of when the data was captured, enabling full history reconstruction. - Deduplication: Before writing to DDS, values are checked against existing data. If identical, no new row is created.
- Current state via
DISTINCT ON: The "current" profile is derived by selecting the most recent row per entity ID.
SAA Table Types
| Suffix | Purpose | Example |
|---|---|---|
*_stream | User action event streams (high volume) | buyer_stream, cookie_stream, seller_stream |
*_profile | Entity characteristics (change-tracked) | buyer_profile, seller_profile, item_profile |
*_registry | Admin-managed reference data | subject_registry, location_registry, tag_registry |
Stream tables always have session_id (user actions happen in sessions).
Profile tables store real-world characteristics that change over time.
Registry tables contain artificial data created by employees.
SAL Aggregates (Pre-calculated)
| Table | Fields | Purpose |
|---|---|---|
current_item_review_stats | item_id, rating_avg, reviews_count | Per-item review statistics |
current_seller_review_stats | seller_id, rating_avg, reviews_count, items_count | Per-seller review statistics |
DDS — Data Vault Pattern
The DDS layer uses Hub/Link/Satellite modeling:
| Type | Prefix | Purpose | Example |
|---|---|---|---|
| Hub | H_ | Business key registration | h_buyer, h_seller, h_item, h_review |
| Link | L_ | Relationships between two entities | l_seller_item, l_buyer_review, l_item_location |
| Satellite | S_ | Entity attributes/characteristics | s_item_name, s_school_phone, s_review_rating |
Key DDS entities (Hubs): h_buyer, h_parent, h_student, h_seller, h_school, h_item, h_review, h_serp, h_cookie, h_photo, h_video, h_location, h_session, h_event
Rules for DDS:
- Hub tables contain only the business key +
event_time - Link tables connect exactly two Hub entities +
event_time - Satellite tables store one attribute per table (6NF) +
event_time - Only
*_profileand*_registrySAA tables feed into Satellite tables *_streamtables feed into Hub and Link tables only (no attributes)- No column in DDS can be null
When Building Features — Database Implications
When adding a new feature, consider:
- Operational tables (Prisma schema): These are the tables your NestJS code reads/writes directly
- Analytical shadow: Each operational write should eventually flow into the SAA layer for analytics
- The Prisma schema is NOT the analytical schema: Prisma models are for the operational database. The SAA/SAL/DDS architecture runs alongside, populated via ETL/CDC processes
- Design Prisma models for operational use, but ensure they capture enough data to feed the analytical pipeline later
Текущее расположение репозиториев
qadam-core/
apps/api/ # NestJS + Fastify backend
packages/shared/ # Zod schemas, constants, backend contracts
packages/prisma/ # Prisma schema, migrations, seed
docs/ # каноническая документация
specs/ # платформенные спеки
qadam-web/
apps/web/ # Next.js frontend
openapi/ # mirror OpenAPI artifact
docs/ # frontend handoff / fallback docs
Backend Module Structure (NestJS)
Each feature module follows the pattern:
src/modules/{feature}/
{feature}.controller.ts # HTTP layer (thin)
{feature}.service.ts # Business logic
{feature}.module.ts # NestJS module definition
repositories/
{feature}.repository.ts # Data access (Prisma)
dto/
create-{feature}.dto.ts # Input validation schemas
{feature}.response.ts # Response DTOs
Frontend Page Structure (Next.js)
Pages use App Router conventions:
app/
page.tsx # Home / Catalog (Server Component)
item/[slug]/page.tsx # Item detail (Server Component)
sellers/[id]/page.tsx # Seller profile (Server Component)
login/page.tsx # Login
register/page.tsx # Registration
me/
page.tsx # Buyer cabinet entry
profile/page.tsx # Buyer profile edit
leads/page.tsx # Buyer leads list
reviews/page.tsx # Buyer reviews
seller/
layout.tsx # Seller dashboard layout
page.tsx # Seller overview
profile/page.tsx # Seller profile edit
items/page.tsx # Seller items list
items/new/page.tsx # Create new item
items/[id]/edit/page.tsx # Edit item
leads/page.tsx # Seller leads
staff/page.tsx # Staff management
admin/
layout.tsx # Admin layout
page.tsx # Admin overview
moderation/items/page.tsx # Item moderation
leads/page.tsx # All leads
reference/
subjects/page.tsx # Subject CRUD
locations/page.tsx # Location CRUD
Database Connection Strategy
Read/Write Splitting
- Write operations (INSERT, UPDATE, DELETE): Use primary PostgreSQL connection
- Read operations (SELECT): Use read replica via
prisma.$replica() - This reduces load on the primary database and improves read performance
Redis Usage
| Use Case | Key Pattern | TTL |
|---|---|---|
| Catalog cache | catalog:{filters_hash} | 5 min |
| Reference data | ref:subjects, ref:locations | 30 min |
| Session | session:{token} | 24 hours |
| Rate limiting | rate:{ip}:{endpoint} | 1 min |
| Popular items | popular:items | 15 min |
| Review stats | stats:item:{id}, stats:seller:{id} | 10 min |
SEO Requirements
- All public pages must be server-rendered (SSR or SSG)
- Each item page must have proper
<title>,<meta description>, Open Graph tags - Slugs must be transliterated from item titles (Cyrillic → Latin)
- Sitemap generation for all published items
- Structured data (JSON-LD) for course/service pages
API Documentation
Текущий base prefix API: /api/v1/.
Источники истины:
- обзорная карта маршрутов:
../api-routes.md - машиночитаемый контракт:
../../apps/api/openapi/openapi.json - runtime Swagger UI:
/api/docs - runtime OpenAPI JSON:
/api/openapi.json
Правило:
- примеры маршрутов внутри старых specs могут содержать исторические имена endpoint-ов;
- перед реализацией всегда сверяйся с
api-routes.mdи OpenAPI artifact.
Infrastructure
Текущий production contour
- product domain:
https://qadam.2fab.app - roadmap portal:
https://qadam-roadmap.2fab.app - runtime:
systemd+ host-levelnginx - API:
127.0.0.1:5001 - Web:
127.0.0.1:3000
Local development ports
| Service | Port |
|---|---|
| API (NestJS) | localhost:5001 |
| Web (Next.js) | localhost:5002 |
| PostgreSQL | обычно localhost:5432 |
| Redis | обычно localhost:6379 |
Rate Limiting
- Global: 100 requests per 60 seconds per IP
Notifications
- Telegram verification endpoints для seller уже есть
- доставка lead notifications в Telegram и Email пока не завершена