გორა.
AI-translation · draft (awaiting native review)
Last-mile Delivery Platform

ტექნიკური სტეკი

არქიტექტურა, სტეკი, სკრიპტები, დეპლოი, უსაფრთხოება, observability.

~30 წუთი წასაკითხი · 6001 სიტყვა

TL;DR

Last-mile Delivery Platform არის Turborepo-ზე აგებული მონორეპოზიტორი თერთმეტი აპლიკაციით და ორი shared-პაკეტით. Backend დაწერილია NestJS 10-ზე (ოცდათექვსმეტი მოდული, ორმოცდაოთხი კონტროლერი, ერთი WebSocket gateway, ორმოცდასამი entity TypeORM-ის გამოყენებით PostgreSQL 16-სა და Redis 7-ის თავზე). Web storefront აგებულია Next.js 14-ზე App Router-ით, კატალოგის ISR-ქეშირებით და სამი ლოკალის i18n-ით. ადმინ პანელი — ცალკე Vite 6 SPA ორმოცდაათი გვერდით და RBAC ოთხ როლზე. სამი Telegram mini-app (sales, dispatch, driver) გაკეთებულია Vite 7 + React 19-ზე და ვალიდაცია გადის initData HMAC-SHA-256-ით. სამი Python ბოტი aiogram 3-ზე API-სთან კომუნიკაციას ახდენს Redis pub/sub-ით. Real-time ხორციელდება Socket.IO-თი ხუთი ბიზნეს-ივენტითა და per-store, per-dispatcher, per-driver, per-customer ოთახებით. Production-ი მუშაობს Docker compose-ში ერთ VPS-ზე, ცხრა სერვისი და nginx Let's Encrypt-ით. CI/CD GitHub Actions-ზე path-based ფილტრით ხელახლა აწყობს მხოლოდ ცვლილებების შემხებ სერვისებს და დეპლოი ხდება GHCR-ის გავლით. უსაფრთხოებისთვის — bcrypt, JWT მცირე-ვადიანი access ტოკენებით და refresh-ის როტაციით, throttler, Helmet, HSTS, HMAC Telegram-ისთვის, OTP Twilio A2P-სა და SMS-Gate Android-ით. Observability-ისთვის — საკუთარი ივენტ-ანალიტიკა client_events-ზე (90 დღე) პლუს rrweb session replays (30 დღე), audit-trail და checkout/login funnel ადმინ პანელში. ტექ-ვალი ღიად ფიქსირდება: ტესტები არ არის, inventory-log შეკვეთის დროს ჯერ არ იწერება, refresh token rotation დასახვეწია.

შინაარსი

მაღალი დონის არქიტექტურა

პლატფორმა არის სრული last-mile delivery სტეკი საქონლის ლოკალური მიწოდებისთვის ბოლო მომხმარებლის კართან. ერთი მონორეპოზიტორის შიგნით ცხოვრობს კლიენტის web storefront, ოპერატორის ადმინ პანელი, სამი Telegram mini-app (მყიდველებისთვის, დისპეტჩერებისთვის და მძღოლებისთვის), ერთი საერთო API და სამი Python ბოტი. გარეთ — nginx SSL-ით და გარე სერვისების ნაკრები: Twilio SMS A2P-ისთვის, SMS-Gate Android დუბლიკატ OTP-არხისთვის, Pushover კრიტიკული ოპერაციული შეტყობინებებისთვის, Telegram Bot API, კრიპტო-გადახდების კოშელეკები და Nominatim გეოკოდინგისთვის. ძირითადი პრინციპი — მინიმალური ცვალებადი ნაწილები და მათ შორის მაქსიმალური განცალკევება მკაფიოდ აღნიშნული კომუნიკაციის წერტილებით: REST endpoint-ები, WebSocket ოთახები და Redis pub/sub არხები.

ქვემოთ — სისტემის ზოგადი სქემა დომენების საზღვრებით და მონაცემთა ნაკადებით.

flowchart LR
  subgraph "Clients"
    Web[Web storefront<br/>Next.js 14]
    AdminUI[Admin panel<br/>React + Vite]
    SalesMA[Sales mini-app<br/>Telegram]
    DispatchMA[Dispatch mini-app<br/>Telegram]
    DriverMA[Driver mini-app<br/>Telegram]
  end

  subgraph "Edge"
    NGINX[nginx + SSL<br/>Let's Encrypt]
  end

  subgraph "Backend"
    API[NestJS API<br/>36 modules]
    WS[WebSocket gateway]
  end

  subgraph "Bots (Python)"
    BotC[Customer bot]
    BotD[Driver bot]
    BotS[Sales bot]
  end

  subgraph "Data"
    PG[(PostgreSQL 16<br/>43 entities)]
    Redis[(Redis 7<br/>cache + pub/sub)]
  end

  subgraph "External"
    Twilio[Twilio A2P]
    SMSGate[SMS Gate<br/>Android]
    Pushover[Pushover]
    TG[Telegram Bot API]
    Crypto[Crypto wallets]
    Maps[Nominatim]
  end

  Web & AdminUI & SalesMA & DispatchMA & DriverMA --> NGINX
  NGINX --> API
  NGINX --> WS
  API <--> PG
  API <--> Redis
  WS <--> Redis
  Redis -.pub/sub.-> BotC & BotD & BotS
  BotC & BotD & BotS --> TG
  API --> Twilio
  API --> SMSGate
  API --> Pushover
  API --> Crypto
  API --> Maps

აქ კარგად ჩანს მთავარი იდეა: API არაფერი იცის Telegram-შეტყობინებების შესახებ, ბოტებმა — ბაზის შესახებ. ნებისმიერი მყიდველის ნოტიფიკაცია — უბრალოდ Redis-არხში პუბლიკაციაა, ხოლო კლიენტამდე მისი მიწოდებით ცალკე პროცესი ხელმძღვანელობს. იგივეა დისპეტჩერის და მძღოლის არხებში. ეს საშუალებას მაძლევს ოპერაციული და პროდუქტული ცვლილებები ერთმანეთისგან გამოვყო: ერთ ადამიანს შეუძლია API-ში შეკვეთების workflow-ის რედაქტირება, მეორეს — ბოტში UX-ის, ისე რომ ერთმანეთს ფეხებს არ აბრუნებენ.

მონორეპო და მისი საზღვრები

პროექტის ძირი მაქსიმალურად მარტივადაა მოწყობილი:

delivery-platform/
├── apps/                    11 აპლიკაცია
│   ├── api/                 NestJS + TypeORM + Postgres + Redis + WebSocket
│   ├── web/                 Next.js storefront — port 3001
│   ├── admin/               Vite SPA — port 3002
│   ├── telegram-mini-app/   Vite + React (dispatcher) — port 3003
│   ├── driver-miniapp/      Vite + React (driver) — port 3004
│   ├── sales-miniapp/       Vite + React (sales)
│   ├── telegram-bot/        Python + aiogram (dispatcher)
│   ├── telegram-driver-bot/ Python + aiogram (driver)
│   ├── telegram-sales-bot/  Python + aiogram (sales)
│   ├── dispatcher/          Expo / React Native (placeholder)
│   └── driver/              Expo / React Native (placeholder)
└── packages/                2 shared package
    ├── shared/              ტიპები, enums, კონსტანტები
    └── ui/                  React კომპონენტები (Button, Input, Card, Badge, Spinner)

Build სისტემა — Turborepo npm workspaces-ის თავზე. ეს ორი პრაქტიკულად მნიშვნელოვან რამეს იძლევა. პირველი — საერთო tsconfig და ერთიანი ტიპების ნაკრები packages/shared-ში (User, Order, Store, OrderStatus, ORDER_TRANSITIONS, SOCKET_EVENTS), რომელსაც იყენებენ frontend-ებიც და API-ც. ეს ნიშნავს, რომ როცა order-ში ახალ ველს ვამატებ ან enum-ში ახალ მნიშვნელობას, შემდეგ build-ზე TypeScript მაჩვენებს, სად ეს ველი არ არის გათვალისწინებული. მეორე — Turbo-ს შეუძლია შეცვლილი ფაილების hash-ის დათვლა და იმ workspace-ების build-ის გამოტოვება, რომელსაც ცვლილებები არ ხებია. CI-სთვის ეს გადაითარგმნება დროის მნიშვნელოვან ეკონომიაში, რადგან ტრივიალური ცვლილებების დროს, ვთქვათ, web-ში, ბოტებსა და mini-app-ებს ხელახლა არ ვაწყობთ.

packages/ui პაკეტი შეგნებულად მცირეა და შეიცავს მხოლოდ ნეიტრალურ ელემენტებს, რომელთაც აზრი აქვთ ნებისმიერ მომხმარებელ აპლიკაციაში: ღილაკი, input, card, badge და spinner. ეს კომპრომისია „დუბლირებას ვიცავთ" და „UI პაკეტს ბელად არ ვაქცევთ, რომელშიც მთელი დიზაინ-ენა გადადის" შორის. რეალური ვიზუალური ენა — Tailwind ტოკენები და უფრო რთული კომპოზიციები — ცალკე ცხოვრობს თითოეულ აპლიკაციაში, რადგან ადმინ პანელსა და storefront-ს პრინციპულად განსხვავებული ამოცანები და UX აქვთ.

ძირის package.json და turbo.json ისეა გამართული, რომ dev-ზე ვაკეთებ ერთ npm run api:dev, npm run web:dev, npm run admin:dev და თითოეული აპლიკაცია იღებს დამოუკიდებელ hot reload-ს. VPS-გარემოში dev-სერვერები მუშაობენ systemd-ით (dev-api, dev-web, dev-admin, dev-sales), და hot reload ცვლილებებს ავტომატურად ხელს ჰკიდებს — გადატვირთვა საჭიროა მხოლოდ .env-ის ცვლილების ან პროცესის გაჭედვის დროს.

API: NestJS, მოდულები, მიგრაციები

Backend არის ყველაზე დიდი აპლიკაცია მონორეპოში: ოცდათექვსმეტი მოდული, ორმოცდაოთხი კონტროლერი და ერთი WebSocket gateway. ოცდათექვსმეტი მოდული — ეს არ არის „იმიტომ რომ ლამაზი ციფრია", არამედ რეალური დომენური დეკომპოზიციის ანარეკლი. თითოეული მოდული შეესაბამება საკუთარ პასუხისმგებლობას და ფლობს entity-ების, სერვისებისა და კონტროლერების მცირე ნაკრებს.

მოდულების ჩამონათვალი კარგ წარმოდგენას აძლევს იმაზე, რა ხდება პლატფორმის შიგნით:

  • დომენური მოდულები შეკვეთისა და კატალოგისთვის: orders, products, categories, brands, cart, time-slots, delivery, stores, reviews;
  • იდენტიფიკაცია და წვდომა: auth, users, roles, employees, customer-plans;
  • ოპერაციული პანელი: admin, admin-promotions, dispatcher, driver-personal, vehicles;
  • მარკეტინგი და შენარჩუნება: banners, campaigns, rewards;
  • ინტეგრაციები და გადახდები: crypto-payments, email, messaging, sms, whatsapp, notifications;
  • ინფრასტრუქტურული: database, geocoding, health, realtime, events, uploads.

ორმოცდასამი entity ფარავს პლატფორმის მთელ ბიზნეს-მოდელს: მომხმარებლები, მაღაზიები, პროდუქტები ვარიანტებით, კალათა, შეკვეთები, შეკვეთის ერთეულები, სტატუსების ლოგები, დროის სლოტები, promo-კოდები, ბანერები, კამპანიები, მიმოხილვები, მძღოლები, ცვლები, კოშელეკები, კრიპტო-დეპოზიტები და ა.შ. განსაკუთრებით საინტერესოა admin ქვედომენი — მის შიგნით ცხოვრობს თერთმეტი დამხმარე entity: cash-drop, checkout-log, client-event, driver-shift, inventory-log, login-log, order-status-history, otp-log, product-audit-log, session-recording, wallet-transaction. არსებითად ეს არის mini-storage მთელი ოპერაციული ანალიტიკისთვის, audit-trail-სა და observability-სთვის.

მოდულის ფაილები მდებარეობენ apps/api/src/modules/<module>/-ში, ხოლო კონკრეტული სერვისები და კონტროლერები თითოეული მოდულის შიგნით სტანდარტული NestJS პატერნით არიან აწყობილი: *.module.ts, *.controller.ts, *.service.ts, entities/*.entity.ts. დამოკიდებულებები გადაიცემა DI-ით, რაც მნიშვნელოვნად ამარტივებს ლოკალურ შეცვლასა და დებაგინგის დროს mock-ებს.

TypeORM და მიგრაციები

შეგნებულად არ დავეყრდენი TypeORM auto-sync-ს კრიტიკული სქემის ცვლილებებისთვის. Auto-sync კარგია ადრეულ ეტაპებზე, მაგრამ პროდაქშენში სახიფათოა: ერთი დაუდევარი სვეტის გადარქმევა იქცევა მონაცემებიანი სვეტის drop-ად. ამიტომ სქემის ყველა მნიშვნელოვანი ცვლილება გაფორმებულია ჩვეულებრივი SQL-მიგრაციად apps/api/src/database/migrations-ში:

  • 20260121-add-cashback.sql
  • 20260329-add-plan-switch-logs.sql
  • 20260330-premium-tier.sql
  • 20260403-premium-tier-settings.sql
  • 20260404-vehicles.sql
  • 20260417-order-item-cost-price.sql

მათ ვტრიალებ ხელით ნათელი თანმიმდევრობით dev-ზე და შემდეგ prod-ზე. ეს ორ უპირატესობას იძლევა. პირველი — მიგრაციებს აქვთ თარიღიანი სახელები, რომელიც კარგად იკითხება code review-სა და git-ში. მეორე — ზუსტად ვიცი, რა მიგრირდება და რა არა, და შემიძლია გავჩერდე, თუ რამე არასწორად მიდის. seed-მონაცემებისთვის არის ცალკე სკრიპტი apps/api/src/database/seeds/seed.ts, რომელიც dev-ბაზას ავსებს ბაზისური კატეგორიებით, ტესტური მაღაზიებითა და ერთი dev-მომხმარებლით.

კონტროლერები და REST

ორმოცდაოთხი კონტროლერი — ეს, ჩვეულებრივ, არ არის მოდულებთან ერთი-ერთთან. ზოგიერთ მოდულს ორი კონტროლერი აქვს: ერთი საჯარო (/api/v1/orders), ერთი admin (/api/v1/admin/orders). სხვაგან კონტროლერები სემანტიკის მიხედვით განცალკევებულია: მაგალითად, events-ში არის public endpoint-ები POST /events (კლიენტური ივენტების batch) და POST /events/replay (rrweb chunks), და ცალკე admin კონტროლერი ფილტრებით, სტატისტიკით, სესიების ძიებითა და search-ანალიტიკით.

გლობალური ValidationPipe ჩართულია main.ts-ში, ვალიდაცია — class-validator-ით DTO-ზე. ეს გამორიცხავს მთელ კლასს ბაგებს „მოვიდა აზრიანი JSON", და ამავე დროს კლიენტს აძლევს ნათელ შეცდომებს: ზუსტად სად არ გაიარა ველმა ვალიდაცია, რა ტიპი იყო მოსალოდნელი, რა მნიშვნელობებია დაშვებული.

Web storefront: Next.js 14, ISR, i18n

Storefront აგებულია Next.js 14-ზე App Router-ით, და მისი ამოცანები იწყება კატალოგის სწრაფი ჩატვირთვით და მთავრდება სრული checkout-ით, ავთორიზაციით, კოშელეკითა და referral პროგრამით. ქვემოთ — ის, რაც ამ ხუთ-ათასიან გვერდზე მართლაც მნიშვნელოვანია.

App Router და i18n

გვერდების ძირი — apps/web/src/app/[locale]/, და თითოეული [locale] მნიშვნელობა — en, ru ან es. ლოკალიზაცია ჩართულია next-intl-ით, ლექსიკონები მდებარეობენ apps/web/messages/<locale>.json-ში. layout-ის დონეზე ვამატებ NextIntlClientProvider-ს, და ყველა კომპონენტი იღებს თარგმანების წვდომას useTranslations-ით. მარშრუტების სეგმენტირება ლოკალის მიხედვით სწორ canonical URL-ებსა და hreflang-ს იძლევა შამანობის გარეშე, პლუს უფლებას იძლევა default ლოკალისთვის preload-პრიორიტეტების დაყენებას.

ISR კატალოგისთვის

კატალოგი — ცხელი გზაა. მყიდველები ხსნიან პროდუქტების და კატეგორიების გვერდებს უფრო ხშირად, ვიდრე რაიმე სხვას, და ეს გვერდები იშვიათად იცვლება inventory განახლებებს შორის. კატალოგის სეგმენტებზე ვაყენებ revalidate: 60, და Next.js წუთში ერთხელ ხელახლა გამოუშვებს HTML-ს. nginx-ის edge-ქეშთან ერთად ეს იძლევა პრაქტიკულად სტატიკურ სიჩქარეს პოპულარულ გვერდებზე, ხოლო კონტენტ-რედაქტორი არ უნდა აწარმოოს build-ის გადატვირთვა პროდუქტის აღწერის შესწორებაზე.

დინამიკური გვერდებისთვის (კალათა, checkout, კაბინეტი, კოშელეკი) ISR არ გამოიყენება — ისინი per-request რენდერდება აქტუალური მონაცემებით.

Dynamic imports და მძიმე კომპონენტები

მთავარი და პროდუქტების გვერდების ნაწილი იყენებს next/dynamic-ს მძიმე ბლოკებისთვის: TestimonialsSection, QRCodeSVG referral კოდებისთვის, lazy-loading motion ანიმაციებისთვის. ეს ამცირებს პირველად JS bundle-ს და აუმჯობესებს LCP-ს — განსაკუთრებით slow 3G-ზე და ნელი CPU-ს მქონე desktop-ებზე. broken dynamic import-ის ეპიზოდის შემდეგ (იხ. 0f6e19b fix(web)) დავაბრუნე სტატიკური import ერთ ბლოკში, რადგან რეალურად bundle-ში მოგებას არ იძლეოდა.

რუკები და გეო-მონაცემები

რუკებისთვის ვიყენებ Leaflet-ს (ღია გადაწყვეტა, კომერციული კვოტების გარეშე). Tile-ებს ვიღებ OSM-პროვაიდერისგან, geocoding — Nominatim-ით. ეს სრულად ფარავს ჩვენს ამოცანებს: მიწოდების მისამართის არჩევა, დაფარვის ზონის ჩვენება და მძღოლის რეალურ-დროის მარკერი.

ავთორიზაცია

Storefront-ი მხარს უჭერს შესვლის სამ მეთოდს: email + პაროლი OTP-ით, Google OAuth 2.0 და Telegram WebApp. Google OAuth რეალიზებულია ისე, რომ affiliateCode პარამეტრი გადის redirect-ის გავლით — ეს დაფიქსდა 311fce3 fix: pass affiliate code through Google OAuth registration-ში. Telegram WebApp ვალიდდება HMAC-SHA-256-ით: backend იღებს sorted query params-ს, გაიყვანს bot secret-ით და ადარებს hash-ს. თუ ემთხვევა — მომხმარებელი ითვლება ავთენტიფიცირებულად, და API აბრუნებს access + refresh-ს.

ანიმაცია და accessibility

გამოიყენება motion/react ბიბლიოთეკა. ყველა ძირითადი ანიმაცია გაშვებულია prefers-reduced-motion-ის შემოწმებით, რათა ჩართული reduced-motion-ის მქონე მომხმარებლებმა მიიღონ სტატიკური ინტერფეისი. სწორ focus-management-სა და aria-ატრიბუტებთან ერთად ეს იძლევა იკითხებად accessibility-ს დიზაინის მსხვერპლის გარეშე.

rrweb და ანალიტიკა

Frontend-ზე ჩართულია rrweb გონივრული პარამეტრებით: ჩაწერა მხოლოდ ავთორიზებული მომხმარებლებისთვის, maskAllInputs: true (პაროლები და მისამართები ჩანაწერში არ მოხვდება), და დროის ლიმიტი 15 წუთი ერთ სესიაზე. ეს — დეტალურად იხ. Observability სექცია ქვემოთ — უზრუნველყოფს პრობლემური სესიების რეპროდუცირებას ადმინ პანელიდან PII-ს გაჟონვის რისკის გარეშე.

პარალელურად მუშაობს custom ანალიტიკა. ფაილი apps/web/src/lib/analytics.ts ინახავს sessionId-ს localStorage-ში, ბაჩავს ივენტებს ხუთ წამში ერთხელ და აგზავნის მათ POST /events-ზე. თუ მომხმარებელი ხურავს tab-ს, ვიყენებთ navigator.sendBeacon-ს, რათა არ დავკარგოთ ბოლო batch-ი. სერვერზე ჩართულია rate limit 10 მოთხოვნა წუთში ამ endpoint-ზე, რაც ერთდროულად იცავს ბოტებისგან და ლეგიტიმურ სცენარებს უპრობლემოდ ტოვებს.

Admin panel: Vite SPA, RBAC, რეპორტინგი

Admin panel — ცალკე Vite 6 SPA React 18-ზე, ორმოცდაათი გვერდით და React Router v6-ზე აგებული შიდა მარშრუტიზაციის სისტემით. რატომ ცალკე აპლიკაცია და არა საერთო კოდი storefront-თან: admin-სა და storefront-ს პრინციპულად განსხვავებული ამოცანები აქვთ. Admin panel — ეს არის ოპერატორის სამუშაო ადგილი, სადაც მნიშვნელოვანია მონაცემთა სიმჭიდროვე, რთული ცხრილები, გრაფიკები და ქმედებები. Storefront — მარკეტინგი და გაყიდვებია, სადაც მნიშვნელოვანია ესთეტიკა, ჩატვირთვის სიჩქარე, marketing metrics. გამიჯვნა ეხმარება ორივე პროდუქტს.

State და server cache

ლოკალური state — Zustand, ზედმეტი slice-ებისა და ცვილისფრის გარეშე. Server state — React Query: invalidation, refetching, optimistic updates. ეს დაყოფა იძლევა მონაცემების სუფთა მოდელს: ყველაფერი, რაც სერვერიდან მოდის, გადის React Query-ით (სწორი staleTime-ით და retry-სტრატეგიებით), და ყველაფერი, რაც UI-ს მართავს (ცხრილის ფილტრები, ღია modal, აქტიური tab), ცხოვრობს Zustand-ში.

გრაფიკები და ანალიტიკა

Recharts — ანალიტიკისა და dashboard-ებისთვის: BarChart Dashboard და Sales გვერდებზე, LineChart Conversion Analytics გვერდზე. ეს არ არის ბაზრის ყველაზე ლამაზი პროდუქტი, მაგრამ ის მსუბუქია, გაფართოებადი და ფარავს ამოცანების 95%-ს გარე დამოკიდებულებების გარეშე. სადაც არ ჰყოფნის — ვწერ custom SVG ვიზუალიზაციებს Recharts-wrapper-ების თავზე.

PDF invoice-ები

Invoice-ებისთვის ვიყენებ jsPDF + autoTable. როცა ოპერატორი წვება „PDF-ის ჩამოტვირთვაზე", კლიენტზე ფორმირდება დოკუმენტი შეკვეთის სრული შემადგენლობით, გადასახადებითა და ხელმოწერით. server-PDF-render-ისა და headless Chrome-ის გარეშე — რაც მნიშვნელოვანია prod-ში, სადაც თითოეული დამატებითი სერვისი ნიშნავს დამატებით უარყოფის წერტილს.

Layouts და როლები

Admin-ის შიგნით სამი განსხვავებული layout კონტექსტია: AdminLayout, DriverLayout, DispatcherLayout. არსებითად ეს არის სამი განსხვავებული პროდუქტი ერთ SPA-ში, მათ შორის გადართვა განისაზღვრება მომხმარებლის როლით. RBAC რეალიზებულია მარშრუტების დონეზე RoleGuard-ით: wrapper-კომპონენტი, რომელიც უყურებს მიმდინარე როლს store-დან და ან რენდერდება შვილებს, ან გადამისამართდება 403-ზე.

ოთხი როლი:

  • admin — სრული წვდომა, მაღაზიის კონფიგურაცია, თანამშრომლების მართვა, ბილინგი.
  • dispatcher — ოპერაციული მუშაობა: შემავალი შეკვეთები, მძღოლების დანიშვნა, მყიდველების მხარდაჭერა.
  • driver — საკუთარი layout მძღოლისთვის შეკვეთების, მარშრუტებისა და შემოსავლის სანახავად.
  • manager — შეზღუდული admin, ჩვეულებრივ ბილინგზე და სისტემურ პარამეტრებზე წვდომის გარეშე.

ხატულები ყველგან — lucide-react. დიზაინ-სისტემა Tailwind 3-ზე სტოის საკუთარი token-ებით tailwind.config.ts-ში. შეგნებულად არ ვიყენებ Material UI-ს მსგავს მზა component-kit-ს, რადგან admin panel უნდა გამოიყურებოდეს როგორც ჩვენი ბრენდის ნაწილი, და არა როგორც კიდევ ერთი Material საიტი.

Telegram mini-app: სამი ზედაპირი

Telegram-ის შიგნით პლატფორმას სამი ცალკე mini-app აქვს, და თითოეული საკუთარ ამოცანას წყვეტს:

  • sales mini-app — ანონიმური კატალოგი, lazy-auth და სწრაფი checkout იმ მყიდველებისთვის, ვინც ბოტიდან შემოდის.
  • dispatch mini-app — დისპეტჩერის გადაადგილებადი სამუშაო ადგილი: შეკვეთების სია, ფილტრები, ქმედებები.
  • driver mini-app — მძღოლის ინტერფეისი: მიმდინარე მიწოდება, მარშრუტი, pickup/delivered აღნიშვნები.

სამივე დაწერილია Vite 7 + React 19 + Tailwind 3-ზე და იყრება ჩვეულებრივ SPA-ებად, რომლებსაც nginx ემსახურება. არა Next.js, რადგან mini-app — ეს მხოლოდ client-side კონტექსტია Telegram WebView-ში, და SSR-ს აქ აზრი არ აქვს.

Telegram WebApp HMAC

mini-app-ში ავთორიზაცია აგებულია initData-ს ვალიდაციაზე. Telegram მომხმარებლის მონაცემებს გადასცემს სტრიქონში, ხელმოწერილს HMAC-SHA-256-ით bot secret-ის გამოყენებით. სერვერზე ვალიდაციის ალგორითმი: query string-ის დაშლა, key-ების სორტირება, კონკატენაცია key=value\n... ფორმატში, HMAC-ის დათვლა და hash-თან შედარება. თუ ემთხვევა — მონაცემები ნამდვილია, შეიძლება ვენდობოდეთ user.id-ს და ავტომატურად დავარეგისტრიროთ მომხმარებელი ან გავხნათ მისი არსებული ანგარიში.

ავტო-რეგისტრაცია მარტივადაა მოწყობილი: თუ tg_id-ით მომხმარებელი არ არის, ვქმნით ახალს phone-ით სახით tg_{id} (ეს pattern განსხვავდება ჩვეულებრივი ნომრებისგან, ამიტომ კოლიზია არ ხდება) და ვაძლევთ token-ს. storefront-ის მხარეს ეს მომხმარებელი შემდეგ შეიძლება მიამაგროს ნამდვილ ნომერს.

ანონიმური კატალოგი და lazy auth

Sales mini-app შეგნებულად აძლევს ანონიმურ წვდომას კატალოგზე. რეგისტრაცია, ფორმები — დასაწყისში არც ერთი არ არის. კალათა cxოვრობს localStorage-ში. ავთორიზაცია მოითხოვება მხოლოდ checkout-ზე — აქ mini-app ამოწმებს Telegram.WebApp.initData-ს და აიღებს token-ს. ეს ამცირებს ფრიქციას ვორონკაში და კარგ კონვერსიას იძლევა.

Real-time Socket.IO-ით

mini-app-ის შიგნით აქტიურადაა გამოყენებული Socket.IO-კლიენტი, რომელიც API-ს უკავშირდება WebSocket gateway-ით. ეს უფლებას აძლევს დისპეტჩერსა და მძღოლს ივენტების მიღების ORDER_CREATED, ORDER_STATUS_CHANGED, ORDER_ASSIGNED, DRIVER_LOCATION_UPDATED, DRIVER_STATUS_CHANGED ზედმეტი polling-ის გარეშე. დეტალურად — Real-time სექციაში ქვემოთ.

Bots: Python, aiogram, Redis pub/sub

სამი ბოტი — ალბათ ყველაზე „მრავალენოვანი" ნაწილი სტეკისა, და ის სპეციალურად ასეთია. Telegram ბოტები — ეს long-polling პროცესებია, რომელთა იმპლემენტაცია უფრო მსუბუქი და სუფთაა Python aiogram-ზე, ვიდრე ცალკე Node.js პროცესის გათრევა tg-grammy-სა და ადაპტერების კუდით. თითოეული ბოტი — ცალკე აპლიკაციაა apps/telegram-bot, apps/telegram-driver-bot, apps/telegram-sales-bot-ში. სტეკი ყველასთან ერთნაირია:

  • Python 3.12;
  • aiogram 3.x — ბოტების თანამედროვე async-ფრეიმვორკი;
  • asyncpg — PostgreSQL-ის native async-დრაივერი ORM overhead-ის გარეშე;
  • redis-py pub/sub-ისთვის.

ვირტუალური გარემო იყრება Docker image-ის შიგნით; dev-build-ში — ცალკე venv-ში ბოტის დირექტორიაში.

განცალკევება Redis-ით

ძირითადი არქიტექტურული გადაწყვეტა აქ — API არაფერი იცის Telegram-შეტყობინებების შესახებ, ხოლო ბოტებს ბაზის შესახებ არაფერი იციან. როცა შეკვეთა მიწოდდება, API აქვეყნებს ივენტს notifications:customer-ში, და მყიდველის ბოტი აიღებს შეტყობინებას არხიდან, ფორმატებს ტექსტს და უგზავნის მყიდველს Telegram-ში. თუ ოპერატორს უნდა ახალი ნოტიფიკაციის დამატება (მაგალითად, „გმადლობთ შეფასებისთვის"), ის ამატებს Redis-ში პუბლიკაციას, და ბოტი წამში იწყებს მიწოდებას API-ს release-ის გარეშე.

არხები სამია:

  • notifications:customer — შეტყობინებები მყიდველს.
  • notifications:dispatcher — fan-out ყველა დისპეტჩერზე (ამჟამად ოთხი).
  • notifications:driver — მძღოლის არხი.

დისპეტჩერის არხში ბოტი კითხულობს შეტყობინებას და ანაწილებს მას Telegram-ში ყველა აქტიურ dispatcher-ანგარიშზე. ასე რეალიზებულია „ყველა ოპერატორზე ხმოვან კავშირზე", როცა შემოდის სასწრაფო შეკვეთა.

გაშვება

Prod-ში თითოეული ბოტი გაშვებულია საკუთარი Docker container-ით, dev-ში — PM2-ით (fork mode, რათა long-poll-ი არ გაიდუბლიროთ და Telegram-ისგან „conflict: terminated by other getUpdates request"-ი არ მივიღოთ). ეს არჩევანიც შემთხვევითი არ არის: long-poll ბოტებისთვის cluster mode თითქმის ყოველთვის ნიშნავს შეტყობინებების დუბლირებას, აქ უკეთესია fork.

მონაცემთა ბაზა და ქეში

მონაცემთა ბაზა — PostgreSQL 16, single instance, ლოკალურად VPS-ზე. მონაცემთა მოცულობა და დატვირთვა ჯერ კიდევ უფლებას იძლევა არ გავყო წაკითხვა და ჩაწერა, და დროზე ადრე სირთულეებს არ ვცვი. სქემა — ორმოცდასამი entity პლუს რამდენიმე custom view რეპორტებისთვის admin-ში.

მოწყობის ძირითადი პრინციპები:

  • მრავალარენდიანობა store_id-ით. ყველა ძირითად entity-ს (პროდუქტები, შეკვეთები, კალათები, მაღაზიის კლიენტი მომხმარებლები) აქვს store_id. სერვისის დონეზე storeId-ით ფილტრაცია — ნებისმიერი query-ის სავალდებულო ნაწილი. ეს უფლებას იძლევა ერთ ინსტანსზე რამდენიმე მაღაზიის განთავსებას ცალკე schema-ების ან ბაზების გარეშე. დეტალურად „არქიტექტურული გადაწყვეტილებების" სექციაში.
  • ინდექსები. ცხელ ველებზე — user_id, order_id, store_id, created_at — ყველგან არის შედგენილი ინდექსები. განსაკუთრებით მნიშვნელოვანია client_events-ზე: დროის სეგმენტისა და მომხმარებლის ტიპიური query-ების დროს ascending-სკანირება იაფი ხდება.
  • ტრანზაქციები inventory-ის ჩამოწერისას. Race condition ერთდროული შეკვეთების დროს დაფიქსდა early ფაზაში და ფიქსდა DB-ტრანზაქციით პირობით WHERE inventory >= qty — ერთ ტრანზაქციაში ვამოწმებთ და ვანახლებთ. თუ პირობა არ შესრულდა, შეკვეთა აბრუნებს 409 Conflict-ს.

Redis 7

Redis-ი ერთდროულად რამდენიმე როლს ასრულებს:

  • Cache. მომხმარებლის session-მონაცემები, ხშირი dictionary-query-ები (მაგალითად, აქტიური store-კონფიგები) და ხშირად გამოყენებული მისამართების ქეშირებული geocode-შედეგები.
  • Pub/sub. ბოტების არხები (იხ. წინა სექცია).
  • Rate limiting. NestJS-ში Throttler იყენებს Redis-store-ს ლიმიტების გასაზიარებლად რამდენიმე worker-პროცესს შორის.
  • Real-time. Socket.IO adapter იყენებს Redis-ს რამდენიმე WebSocket gateway ინსტანსს შორის შეტყობინებების კოორდინაციისთვის.

ერთ Docker container-ში Redis ემსახურება ოთხივე სცენარს. ეს მუშაობს, რადგან ჩვენი Redis-დატვირთვა მისი შესაძლებლობების პროცენტებს შეადგენს.

Real-time: WebSocket gateway და ოთახები

WebSocket gateway აგებულია Socket.IO-ზე და ცხოვრობს apps/api/src/modules/realtime-ში. ხუთი ძირითადი ივენტი ფარავს real-time კონტრაქტს სერვერსა და კლიენტებს შორის:

  • ORDER_CREATED — შეკვეთა შექმნილია;
  • ORDER_STATUS_CHANGED — სტატუსი შეიცვალა;
  • ORDER_ASSIGNED — დანიშნულია მძღოლი;
  • DRIVER_LOCATION_UPDATED — განახლდა მძღოლის გეო-პოზიცია;
  • DRIVER_STATUS_CHANGED — მძღოლის სტატუსი.

თითოეული ივენტი იგზავნება ერთ ან რამდენიმე ოთახში პრინციპით „მიმღები თვითონ ხდება გამომწერი":

  • store:{storeId} — მაღაზიის საერთო ოთახი მისი ყველა თანამშრომლისთვის;
  • dispatcher:{userId} — კონკრეტული დისპეტჩერის პირადი ოთახი (მაგალითად, მასზე შეკვეთების დანიშვნისთვის);
  • driver:{userId} — მძღოლის პირადი ოთახი;
  • customer:{userId} — მყიდველის პირადი ოთახი სტატუსების ნოტიფიკაციებისთვის.

ეს იძლევა შეტყობინებების გამჭვირვალე სემანტიკას: შემიძლია გამოვაქვეყნო ORDER_ASSIGNED driver:42-ში და ზუსტად ვიცოდე, რომ შეტყობინებას მიიღებს მხოლოდ ეს მძღოლი და არა მთელი გუნდი. ამავე დროს — დავადუბლირო store:7-ში, რათა ამ მაღაზიის ყველა დისპეტჩერმა დაინახოს განახლება თავის სიებში.

Geocoding და ETA-ს გაანგარიშება — ცალკე სერვისია API-ში, Nominatim-ის (OSM) გამოყენებით. როცა შეკვეთა იქმნება კოორდინატებით, ETA გაიანგარიშება მანძილით, ტრანსპორტის ტიპითა და საბაზისო კოეფიციენტებით, შემდეგ ნახლდება ყოველი DRIVER_LOCATION_UPDATED-ის დროს.

Order state machine

შეკვეთის მდგომარეობები აღწერილია packages/shared/src/orders.ts-ში კონსტანტა ORDER_TRANSITIONS-ით, რომელიც დეკლარაციული state machine-ის მსგავსად მუშაობს. თითოეული გადასვლა მოწმდება orders სერვისში სტატუსის შეცვლამდე. თუ გადასვლის მცდელობა არასწორია — ისვრება დომენური შეცდომა, და API აბრუნებს 422-ს. ეს იცავს ისეთი ბაგებისგან, როგორიცაა „დისპეტჩერმა შემთხვევით ხელახლა გახსნა უკვე მიწოდებული შეკვეთა" და race condition-ებისგან, როცა ორი განსხვავებული აქტორი ცვლის სტატუსს პარალელურად.

stateDiagram-v2
  [*] --> PENDING: created by client
  PENDING --> CONFIRMED: payment confirmed
  PENDING --> CANCELLED
  CONFIRMED --> READY: packed
  READY --> ASSIGNED: driver assigned
  ASSIGNED --> PICKED_UP: driver picked up
  PICKED_UP --> DELIVERED: delivered
  DELIVERED --> [*]
  CONFIRMED --> CANCELLED
  READY --> CANCELLED
  ASSIGNED --> CANCELLED
  PICKED_UP --> CANCELLED: refund flow

თითოეული გადასვლისთვის order_status_history-ში იწერება ჩანაწერი from_status, to_status, actor_id და actor_role-ით. ეს ფიქსირდება შეკვეთის სრულ ქრონოლოგიას, და ადმინ პანელში ვრენდერდებათ ვიზუალური timeline-ით. სასარგებლოა ანალიტიკისთვის (რამდენ ხანს ჰკიდია შეკვეთა ყოველ სტატუსზე) და ინციდენტების გამოძიებისთვის.

ინფრასტრუქტურა: dev და prod

Dev: VPS7

დეველოპერის გარემო — ცალკე VPS Ubuntu-თი, რომელზეც ცხოვრობს systemd-unit-ები ყველა ძირითადი dev-სერვერისთვის:

  • dev-api — port 3000, NestJS watch-რეჟიმში;
  • dev-web — port 3001, Next.js dev-server;
  • dev-admin — port 3002, Vite dev-server;
  • dev-sales — port 3005, Vite sales mini-app-ისთვის.

PostgreSQL 16 მუშაობს ლოკალურად 5432-ზე, Redis — Docker-ის შიგნით 6379-ზე. Hot reload მუშაობს ყველა აპლიკაციაში, ამიტომ კოდის ცვლილების დროს ხელით არაფერს ვტრიალებ. გადატვირთვა საჭიროა მხოლოდ .env-ის შეცვლის ან პროცესის გაჭედვის დროს.

Build ლოკალურად არ კეთდება. საერთოდ. dev-სერვერზე შეგნებულად არ არის TypeScript-კომპილატორი production-რეჟიმში და არ არის vite/next build. ნებისმიერი npm run build ან tsc ამცირებს მეხსიერებას და ანელებს დანარჩენ პროცესებს. Build — CI-ის ამოცანაა.

Prod: VPS6 Docker compose-ით

Prod-სერვერი — ცალკე VPS, რომელზეც ყველაფერი მუშაობს Docker compose-ში. ფაილი — /opt/delivery/docker-compose.prod.yml. სერვისი ცხრაა, ყველა pull-დება GHCR-დან:

Serviceაღწერა
apiNestJS API
webNext.js storefront
adminVite SPA admin
sales-appVite sales mini-app, port 4005
sales-botPython sales-bot
dispatchVite dispatcher mini-app, port 3003
driver-miniappVite driver mini-app, port 3004
telegram-botPython customer/dispatcher-bot
telegram-driver-botPython driver-bot

პლუს postgres, redis და nginx (80/443) კონტეინერები Let's Encrypt-სერტიფიკატებით. Nginx — reverse proxy docker-სერვისებზე და SSL-ტერმინატორი. ის ასევე ახდენს HTTP/2-სა და Brotli-ის რეალიზაციას სტატიკისთვის.

ძირითადი ნიუანსი: docker compose restart არ კითხულობს ხელახლა .env-ს. ეს ხაფანგია, რომელშიც ადვილად ვარდები. ENV ცვლადების შეცვლის შემდეგ საჭიროა docker compose -f docker-compose.prod.yml down <service> და შემდეგ up -d <service> კონკრეტულად ამ სერვისისთვის. ეს დავაფიქსე პროექტის CLAUDE.md-ში, რათა ხელახლა არ დავაბიჯო ბილიკებზე.

Image-ები და registry

ყველა image ქვეყნდება GitHub Container Registry-ში: ghcr.io/<org>/delivery-platform/<service>. თითოეული სერვისი ტეგდება ორი მარკერით — latest და sha-<short>. compose-ფაილში ვამაგრებთ latest-ს, რადგან ჩვენი დატვირთვისა და SLA-ისთვის ეს არის ნორმალური კომპრომისი მოქნილობასა და პროგნოზირებადობას შორის; თუ ოდესმე ეს პრობლემა გახდება, გადავერთვით sha-ზე.

Dockerfiles

თითოეულ აპლიკაციაში არის თავისი Dockerfile (სულ ცხრა). ეს ჩვეულებრივი multi-stage build-ებია: build stage node:20-alpine-ით JS-სერვისებისთვის და python:3.12-slim-ით ბოტებისთვის, შემდეგ — runtime stage dev-დამოკიდებულებების გარეშე. Image-ები გამოდის კომპაქტური და სწრაფად დეპლოი ხდება.

Compose-ფაილები

რეპოზიტორიში ცხოვრობს სამი compose-ფაილი — სხვადასხვა ამოცანებისთვის:

  • docker/docker-compose.yml — მინიმალური dev-სტეკი: მხოლოდ PostgreSQL და Redis. მოსახერხებელია, თუ დეველოპერი ლოკალურად მუშაობს და უნდა მხოლოდ ინფრასტრუქტურის გაშვება.
  • docker-compose.yml (root) — სრული ლოკალური სტეკი: postgres, redis და ცხრავე აპლიკაცია. გამოიყენება ინტეგრაციული შემოწმებისთვის.
  • docker-compose.prod.yml — production-კონფიგი რეალური port-ებით, healthcheck-ებითა და nginx-ით.

Nginx და დომენები

Nginx-კონფიგი აღწერს შვიდ დომენს და ახდენს მათ proxy-ს კონკრეტულ docker-სერვისებზე:

  • app.platform.com — web storefront;
  • admin.platform.com — admin;
  • api.platform.com — API;
  • shop.platform.com — sales mini-app;
  • dispatch.platform.com — dispatcher mini-app;
  • driver.platform.com — driver mini-app.

(რეალური დომენები გამოტოვებულია NDA-ის გამო. ფაქტობრივ კონფიგურაციაში თითოეული სახელი — ცალკე server-block SSL-სერტიფიკატით Let's Encrypt-დან და proxy_pass-ით საჭირო upstream-ზე.)

CI/CD: path-based filtering და GHCR

ფაილი — .github/workflows/docker-build.yml. Trigger — push main-ში. რა არის შიგნით:

flowchart TB
  Dev[VPS7 dev<br/>Hot reload]
  PR[git push to main]
  GHA[GitHub Actions<br/>Smart path filter]
  CHANGED{Changed services?}
  Build[Docker build per service]
  GHCR[(GHCR registry)]
  SSH[SSH to VPS6<br/>via appleboy/ssh-action]
  DC[docker compose pull + up -d<br/>only changed services]
  Prod[VPS6 prod<br/>9 containers + nginx]

  Dev --> PR
  PR --> GHA
  GHA --> CHANGED
  CHANGED -->|yes| Build
  CHANGED -->|no| End([skip])
  Build --> GHCR
  GHCR --> SSH
  SSH --> DC
  DC --> Prod

Workflow იწყება dorny/paths-filter@v3-ით, რომელიც უყურებს commit-ში ცვლილებებს და ამბობს, ცხრა build-შესაძლო სერვისიდან რომელი უნდა მართლაც ხელახლა აიწყოს (api, web, admin, dispatch, driver-miniapp, telegram-bot, telegram-driver-bot, sales-app, sales-bot). მაგალითად, ცვლილება apps/web/src/...-ში შეეხება მხოლოდ web-ს, და CI ხელახლა ააწყობს ზუსტად web-ს და დანარჩენს იმავე ვერსიაზე დატოვებს. ეს ეკონომიას უყოფს დაახლოებით 5–10 წუთს ყოველ push-ზე და ერთდროულად ამარტივებს rollback-ს: თითოეული სერვისი ვერსირდება დამოუკიდებლად.

ძირითადი ნაბიჯები path-filter-ის შემდეგ:

  1. Build per service. ყოველი შეცვლილი სერვისისთვის GitHub Actions უშვებს docker build-ს სწორი Dockerfile-ით და ტეგით ghcr.io/<org>/delivery-platform/<service>:latest პლუს :sha-<short>.
  2. Push to GHCR. Image-ები ქვეყნდება GitHub Container Registry-ში. CI-ს წვდომა მასთან გამართულია GITHUB_TOKEN-ით packages: write-ით.
  3. SSH VPS6-ზე. appleboy/ssh-action მიდის prod-სერვერზე გასაღებით, ასრულებს docker compose -f /opt/delivery/docker-compose.prod.yml pull <service>-სა და შემდეგ up -d <service>-ს — მხოლოდ შეცვლილი სერვისებისთვის.
  4. Healthcheck. Restart-ის შემდეგ compose ანიჭებს სერვისს დროს healthcheck-ის შესასრულებლად. თუ ის მწვანე არ არის — ამას prod-ში მაშინვე ვხედავთ Pushover-ში.

ამ pipeline-ში უსაფრთხო წერტილები:

  • secrets ინახება GitHub Actions-ში (GHCR_TOKEN, VPS_SSH_KEY, DEPLOY_HOST, DEPLOY_USER); არაფერი არ არის hardcoded რეპოში;
  • ssh-action იყენებს ed25519-key-ს, რომელიც პროვიზდება მხოლოდ CI-runner-ში და არსად სხვაგან არ ჩანს;
  • გზა docker-compose.prod.yml-სა და სერვისების სია — workflow-ის ერთადერთი რამ, რომელსაც is prod-ზე ეხება.

სკრიპტები და ოპერაციული უტილიტები

ძირის scripts/ დირექტორიაში ცხოვრობს მცირე ნაკრები სასარგებლო უტილიტებისა:

  • backfill-geocoding.sh — geocoding-მონაცემების packet backfill. იღებს ყველა შეკვეთას, რომელსაც lat/lng არ აქვს, მიდის Nominatim-ში, ნორმალიზებს მისამართს და ჩაიწერს კოორდინატებს უკან. გამოიყენება ერთხელ მისამართების სქემის ცვლილებების შემდეგ ან ძველი მონაცემების import-ისას.
  • screenshot.js და help-screenshots.mjs — help-გვერდებისთვის screenshot-ების გენერაცია. უკავშირდება dev-stand-ს, აკეთებს UI-ის snapshot-ს და ინახავს PNG-ს.
  • inject-help-images.mjs — ამ image-ების ჩამატება help-კონტენტში render-ის შემდეგ.
  • rescale-menu-icons.js — menu-icons-ის დამუშავება სტანდარტულ ზომაზე ყველა აპლიკაციისთვის.
  • setup-api.sh — API-ის ინსტალაცია: მიგრაციები, seed და .env-ის ბაზისური კონფიგურაცია.
  • test-flows.sh — smoke-test სცენარების ნაკრები: მომხმარებლის შექმნა, შეკვეთის გაფორმება, მიწოდების აღნიშვნა.

ეს არ ცდილობს სრული ავტომატიზაციის პრეტენზიას, მაგრამ აშორებს რუტინას 10–15 განმეორებადი ამოცანისგან, რომელიც sprint-ში ერთხელ ხდება.

უსაფრთხოება

უსაფრთხოება მიკრულია რამდენიმე დონეზე. პუნქტებს ფენების მიხედვით გადმოვცემ, რადგან უსისტემო სია „გვაქვს Helmet" ჩვეულებრივ არაფერს ხსნის.

იდენტიფიკაცია და პაროლები

  • პაროლები ჰეშდება bcrypt-ით cost factor 10-ით. ეს კონკრეტული კომპრომისია შესვლის სიჩქარეს (~50ms თანამედროვე CPU-ზე) და brute-force-ის წინააღმდეგ მდგრადობას შორის.
  • JWT access-token-ები — მცირე-ვადიანი, 15 წუთი (ისტორიულად იყო 30 დღე; აუდიტის შემდეგ დაფიქსდა, იხ. „ტექ-ვალი" სექცია).
  • Refresh-token-ები გრძელ-ვადიანი, rotation-ით: ყოველი გამოყენება გენერირებს ახალ refresh-ს და ინვალიდაციას უკეთებს წინას. ეს ამცირებს შეტევის ფანჯარას refresh-ის გაჟონვის შემთხვევაში.
  • OTP-კოდები Twilio A2P-ით (TCR vetting-ის ქვეშ რეგისტრირებული campaign) და SMS-Gate Android backup-არხად. Round-robin ორ მოწყობილობას შორის, retry failure-ის დროს.
  • Telegram WebApp HMAC-SHA-256 — აღწერილია mini-app სექციაში.

ტრანსპორტი და header-ები

  • HTTPS ყველგან, გამონაკლისების გარეშე. Let's Encrypt გამოუშვებს სერტიფიკატებს შვიდი დომენისთვის.
  • HSTS ჩართულია max-age=31536000-ით და includeSubDomains-ით. ეს ამაგრებს HTTPS-ს ერთი წლით წინ, და backend ასევე აბრუნებს header-ს ყოველი response-ის დროს Helmet-ით.
  • Helmet ჩართულია main.ts-ში: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Strict-Transport-Security, Referrer-Policy: strict-origin-when-cross-origin.
  • CORS — whitelist კონკრეტულ დომენებზე (dispatch.platform.com და driver.platform.com mini-app-ისთვის, პლუს admin და storefront). არანაირი *.

Rate limit

  • @nestjs/throttler — throttling მგრძნობიარე endpoint-ებზე: auth (login/register/forgot-password), events public (10/min), events replay (6/min) და სხვა ადგილებზე, სადაც სხვაგვარად შესაძლებელია API-ის დახეთქვა მოთხოვნებით.
  • API-ზე გლობალური ლიმიტი უფრო რბილია, რათა ლეგიტიმური კლიენტები ზღვარს არ მიადგნენ.

კრიპტო-გადახდები

ეს ცალკე ისტორიაა, რადგან კრიპტოში ინვალიდაცია და ხელმოწერები ჩვეულებრივ ყველაზე სარისკო ნაწილია. მიდგომა:

  • BTC — derivation xpub-key-დან ფრენაში, ახალი მისამართი ყოველ დეპოზიტზე. პრივატული key არსად არ ინახება API-ში; xpub დევს დაცულ config-სექციაში.
  • ETH/USDT-ERC20/USDC-ERC20 — wallet-ები გენერირდება პროგრამულად ბიბლიოთეკებით და ინახება პრივატული key-ით, შიფრირებულით AES-256-ით ENV-ცვლადი master-key-ისგან.
  • TRC-20 (USDT TRON-ზე) — ცალკე არხი ETH-ის ანალოგიით.
  • ყოველი შემოსავალს API აკონტროლებს შესაბამის blockchain-ს RPC-პროვაიდერით და ინახავს wallet_transaction-ს დასტურებით.

ქმედებების ლოგები

ნებისმიერი მგრძნობიარე ქმედება ლოგდება:

  • login_log — ვინ, როდის, რომელი IP-დან, წარმატება/fail;
  • otp_log — OTP-ის გაგზავნა და ვალიდაცია;
  • product_audit_log — ვინ რომელ ველს ცვლიდა პროდუქტში;
  • wallet_transaction — ყველა დეპოზიტი და ჩამოწერა.

Audit-trail admin-ში იყენებს product_audit_log-ს პროდუქტის ცვლილებების მშვენიერი ისტორიის render-ისთვის „smart grouping"-ით: ერთი მომხმარებლის სერია ცვლილებების მცირე დროში გროვდება ერთ ბლოკად, და მნიშვნელობებისთვის render-დება კონტექსტური ფორმატირება (price → $12.99, category_id → Tinctures).

Observability და ანალიტიკა

დაკვირვება — ცალკე graph-ია, რომელიც ნელ-ნელა ავაშენე პროექტის ზრდის შესაბამისად. ამჟამად ფარავს სამ კლასს ამოცანებს: პროდუქტის ანალიტიკას, ინციდენტის გამოძიებას და ოპერაციულ შეტყობინებებს.

client_events — კლიენტის tracking (90 დღე)

ცხრილი client_events ინახავს ყველა კლიენტის ივენტს ბოლო 90 დღის განმავლობაში:

  • page_view — გადასვლები;
  • product_view — card-ის ნახვა;
  • cart_add, cart_remove, cart_update_qty — კალათა;
  • favorite_add, favorite_remove — საყვარელი;
  • search, search_no_results — ძებნა (მეორე განსაკუთრებით ღირებულია გამოტოვებული მოთხოვნების აღმოსაჩენად);
  • checkout_start, place_order, success, error, zone_unavailable — checkout-ვორონკა;
  • api_error, js_error — კლიენტური შეცდომები.

ეს — საფუძველი ყველაფრისა: პროდუქტის ანალიტიკის, ვორონკების კონვერსიის, შეცდომების მონიტორინგის. Public-endpoint POST /events იღებს batch-ებს 50-მდე ივენტამდე და rate-limit 10/min, რათა დაიცვას ცხრილი bot-ტრაფიკისგან. Cron-job ასუფთავებს 90 დღეზე ძველ ივენტებს ყოველდღე 3:00-ზე.

session_recordings — rrweb (30 დღე)

session_recordings ინახავს ავთორიზებული მომხმარებლების სესიების rrweb-chunk-ებს. Chunk-ები შემოდიან POST /events/replay-ში (rate limit 6/min). ყველა input მასკირებულია (maskAllInputs: true), და ერთი ჩანაწერი 15 წუთიანი აქტივობით შეზღუდულია. Cron ასუფთავებს 30 დღეზე ძველებს 4:00-ზე.

Admin-ში არის გვერდი /activity-log ოთხი tab-ით:

  • All events — ფილტრაცია მომხმარებლის, ივენტის ტიპის, პერიოდის მიხედვით;
  • Errors — JS-ისა და API-ის შეცდომები კონტექსტით;
  • Search Analytics — ყველაზე პოპულარული მოთხოვნები და top-10 უშედეგო;
  • User Journey + Replay — კონკრეტული მომხმარებლის გზა და ღილაკი „სესიის ჩვენება", რომელიც ხსნის rrweb-player-ს storage-დან chunk-ებზე.

ეს მკვეთრად აჩქარებს ინციდენტის გამოძიებას. როცა ოპერატორი ამბობს „შეკვეთა დავარდა", ვხსნი მის სესიას და ვხედავ ნაბიჯებს, რომლებზეც მოხდა js_error ან 422 API-დან.

Audit trail და checkout funnel

client_events-ის გარდა, გვაქვს რამდენიმე სპეციალიზებული ცხრილი-ლოგი:

  • checkout_log — ყოველი checkout-ნაბიჯი კონტექსტით (მისამართი, არჩეული მეთოდი, საბოლოო თანხა);
  • login_log, otp_log — აღწერილია ზემოთ;
  • product_audit_log — პროდუქტების ცვლილებები;
  • wallet_transaction — ფინანსური ოპერაციები.

Admin-ში ამ მონაცემების საფუძველზე იქმნება რამდენიმე funnel-რეპორტი:

  • registration_funnel — კონვერსია ტელეფონის შეყვანიდან OTP-ის დადასტურებამდე;
  • login_funnel — კონვერსია login-ის მცდელობიდან წარმატებულ შესვლამდე;
  • checkout_funnel — ძირითადი ფულადი funnel: cart → address → payment → place_order → success.

დავამატე ცალკე „Issues monitor" — ეკრანი, რომელზეც ჯამდება checkout failures-ის ძირითადი მიზეზები (zone unavailable, payment declined, OTP expired), JS-შეცდომები და API-შეცდომები ბოლო 24 საათისთვის. ეს სწრაფი გზაა deploy-ის შემდეგ რეგრესიის შესამჩნევად.

Pushover

ოპერაციული შეტყობინებებისთვის ვიყენებ Pushover-ს. Hardcode-ით ვაყენებ ზუსტად ვისაც სჭირდება — ეს არის ოთხი დისპეტჩერი და ორი მძღოლი apps/api/src/modules/notifications/pushover.service.ts-ში. Trigger-ები:

  • ახალი შეკვეთა — priority=2 (loud, ზარები მანამ, სანამ არ დადასტურდება, 30-წამიანი retry). შეკვეთა არ უნდა დაიკარგოს.
  • შეკვეთა მძღოლზე დანიშნული — priority=2 კონკრეტული მძღოლისთვის.
  • შეკვეთა მიწოდდა — priority=1 (normal) დისპეტჩერებისთვის.

ეს ხურავს ოპერაციულ SLA-ს რთული alert სისტემის გარეშე: თუ შეკვეთა შექმნილია, მაგრამ 30 წამში არ დადასტურდება, დისპეტჩერები იღებენ ზარ-მაღვიძარას.

Cron-job-ები

API-ის შიგნით ცხოვრობს ორი მარტივი ყოველდღიური cron-job:

  • 3:00 — DELETE FROM client_events WHERE created_at < NOW() - INTERVAL '90 days';
  • 4:00 — DELETE FROM session_recordings WHERE created_at < NOW() - INTERVAL '30 days'.

ეს ცხრილებს გონივრულ ზომასა და პროგნოზირებად სიჩქარეებს უნარჩუნებს.

ინტეგრაციები

პლატფორმაში ჩართულია რამდენიმე გარე სერვისი, და თითოეული კონკრეტულ ამოცანას ხურავს. არ ვცდილობ ერთის მეორით ჩანაცვლებას: SMS, email, Pushover და Telegram — განსხვავებული არხებია სხვადასხვა SLA-ითა და ღირებულებით.

  • Twilio A2P — SMS-არხი production-ში. Campaign რეგისტრირებულია და ელოდება TCR vetting-ს. დადასტურების შემდეგ გახდება SMS-ში OTP-ის ძირითადი გზა.
  • SMS-Gate Android — ორი მოწყობილობა (2NROCH, M7TXQY) სხვადასხვა ნომრებით. Round-robin დატვირთვის გასანაწილებლად და retry ერთ-ერთის უარის დროს. გამოიყენება OTP-ისთვის რეგისტრაციისა და შესვლის დროს. არ გამოიყენება forgot-password-ისთვის — იქ email-ია.
  • Email (SMTP) — მხოლოდ forgot-password-ისთვის. სხვა email-ნოტიფიკაციები პლატფორმას არ აქვს, და ეს შეგნებულია: ყოველი დამატებითი არხი — დამატებითი spam და უარის წერტილია.
  • Pushover — აღწერილია ზემოთ.
  • Telegram Bot API — სამი ბოტი, ყველა aiogram-ით.
  • Google OAuth 2.0 — register/login web-ისთვის. გადასცემს affiliateCode-ს redirect-ის გავლით.
  • Crypto wallets — BTC xpub derivation-ით, ETH, USDT-ERC20, USDC-ERC20, TRC-20.
  • WhatsApp — multi-node gateway: 5-მდე node, round-robin sticky-session-ით (ერთი და იგივე dialog ერთ node-ზე გადის). existsCache: Map ინახავს ტელეფონის შემოწმებას WhatsApp-ის არსებობაზე, რათა ყოველ შეტყობინებაზე node-ი არ დავცინცილოთ. (აქვე — ცნობილი ტექ-ვალი: cache eviction-ის გარეშე, იხ. „ტექ-ვალი".)
  • Geocoding (Nominatim/OSM) — ყველა მისამართისთვისა და მიწოდების ზონებისთვის. კომერციული კვოტების გარეშე.

messaging მოდულში გამართულია ერთიანი ლოგიკა „WhatsApp first, SMS fallback": თუ მომხმარებელს WhatsApp აქვს, ვუგზავნით იქ; თუ არა — SMS. ეს ამცირებს ნოტიფიკაციების ღირებულებას და აუმჯობესებს delivery-rate-ს.

არქიტექტურული გადაწყვეტილებები

აქ — ათი ძირითადი გადაწყვეტილება და მათი დასაბუთება. დიდი ნაწილი — ეს ის გადაწყვეტილებებია, რომელიც ერთხელ მივიღე და აღარ დავბრუნდი მათთან, რადგან მუშაობენ.

1. Monorepo Turborepo-ზე

ალტერნატივა — რამდენიმე ცალკე რეპოზიტორი API-სთვის, web-ისთვის, admin-ისთვის და თითოეული mini-app-ისთვის. Monorepo ავირჩიე სამი მიზეზის გამო:

  • საერთო ტიპები (Order, User, OrderStatus, SOCKET_EVENTS) ცხოვრობენ packages/shared-ში და ავტომატურად სინქრონიზდებიან API-სა და frontend-ებს შორის; enum-ის ერთი ცვლილება მაშინვე აჩვენებს ყველა TypeScript-შეცდომას;
  • Turbo path-filter აჩქარებს CI-ს: web-ში ცვლილებებზე ბოტებსა და API-ს ხელახლა არ ააწყობს;
  • ერთი ბრძანება npm install და დამოკიდებულებების შეთანხმებული ვერსიები; React-ისა და Tailwind-ის კონფლიქტური ვერსიების ჯოჯოხეთი არ არის.

ფასი — ცოტა უფრო მძიმე repo და გუნდის სწავლების საჭიროება Turbo-ბრძანებებში. ჩემი მასშტაბისთვის ეს პირველივე კვირაში ანაზღაურდება.

2. TypeORM + ხელით მიგრაციები

TypeORM — ყველაზე მომწიფებული ORM Node-ეკოსისტემაში TypeScript-first ტიპიზაციით. Auto-sync-ს არ ვიყენებ: ნაცვლად — ხელის SQL-მიგრაციები. ეს ორ უპირატესობას იძლევა:

  • ზუსტად ვიცი, რა მიგრირდება, და შემიძლია გავჩერდე;
  • მიგრაციების diff-ები იკითხება review-ის დროს.

ალტერნატივებმა (Prisma, Drizzle) ფაქტორების ჯამში არ მომიგო. Prisma — შესანიშნავი builder, მაგრამ მიგრაციებთან ხელის სამუშაოს ვამჯობინებ. Drizzle ლამაზია, მაგრამ პროექტის დაწყებისას ჯერ კიდევ ახალგაზრდა იყო.

3. Multi-tenant store_id-ით

ყველაზე მარტივი გადაწყვეტა multi-tenant-ისთვის — ცალკე ბაზები ან schema-ები. ავირჩიე store_id სვეტი ძირითად ცხრილებზე, რადგან:

  • ერთი სერვერი ემსახურება რამდენიმე მაღაზიას და ფიზიკური იზოლაცია არ არის საჭირო;
  • store_id-ით ფილტრაცია query-ის დონეზე სრულდება და ადვილად ტესტდება;
  • backup-ები და მიგრაციები — ერთი ბრძანებით მთელ ბაზაზე.

ფასი — უნდა გავფილტრო დისციპლინირებულად. ვხურავ ამას service-დონის guard-ით: ნებისმიერი findOrders იღებს storeId-ს სავალდებულო არგუმენტად, და დავიწყება შეუძლებელია.

4. Redis pub/sub ბოტებისთვის

ბოტები — ცალკე პროცესებია. შეეძლოთ API-სთან REST-ით კავშირი, მაგრამ მაშინ ცრუ-სერვისულ auth-ს გავაკეთებდი (cross-service tokens) და eventual-consistency-ს დავამუშავებდი. Pub/sub უფრო მარტივია:

  • API აქვეყნებს ივენტს notifications:customer-ში;
  • ბოტი იღებს, ფორმატებს და აგზავნის;
  • API არაფერი იცის Telegram-ის შესახებ, ბოტი არაფერი იცის ბაზის შესახებ.

მინუსი — საჭიროა შეტყობინებების სქემის ნათლად აღწერა. ვინახავ მას packages/shared/src/notifications.ts-ში TypeScript-ტიპებად და Python-მხარეს pinner-ი JSON-სქემით.

5. rrweb session replay-ისთვის

ლოგებითა და მომხმარებლების აღწერით bag-ების რეპროდუცირება — ეს სუსტი გადაწყვეტაა. როცა ოპერატორი მაჩვენებს „აი, კლიენტს არაფერი მუშაობს", ვხსნი მის სესიას rrweb-ში და ვხედავ, რა მოხდა. maskAllInputs: true-ით ეს უსაფრთხოა PII-სთვის, და 15-წუთიანი დროის ლიმიტი ხელს უშლის გიგაბაიტიან ჩანაწერებს.

6. Custom audit-trail admin-ში (და არა TypeORM-history)

TypeORM-ს შეუძლია ისტორიის შენახვა subscriber-ებით, მაგრამ „nepe" ისტორია — ეს diff-ლოგია, რომელიც რთულად იკითხება. ავირჩიე ჩემი product_audit_log smart-grouping-ით (ერთი მომხმარებლის ცვლილებებს მცირე დროში აერთიანებს) და კონტექსტური ფორმატირებით (price → $12.99, category_id → კატეგორიის სახელი). ეს ადმინს მოსახერხებელ ეკრანს იძლევა „აი, ამ პროდუქტის ისტორია", ნაცვლად JSON-diff-ების გროვისა.

7. Pushover priority=2 კრიტიკული ივენტებისთვის

priority=2 ნიშნავს, რომ Pushover ყოველ 30 წამში დარეკავს მომხმარებელს, სანამ ის არ დაადასტურებს. ეს — ერთადერთი არხია, რომელიც რეალურად უზრუნველყოფს ოპერაციული შეტყობინების მიწოდებას. SMS იკარგება, push Telegram-ში შეიძლება გაჩუმდეს, email — ეს საერთოდ არ არის შეტყობინება. Pushover priority=2 ჩვენთან „ახალი შეკვეთა"-სა და „მძღოლზე დანიშვნა"-ზე დგას, და მთელი დროის განმავლობაში ერთი შეკვეთაც არ გამოგვრჩა.

8. Path-based filtering CI-ში

მის გარეშე CI ხელახლა ააწყობდა ცხრავე სერვისს ყოველ push-ზე. მისით — მხოლოდ იმათ, რომელიც რეალურად შეიცვალა. web-ში ცვლილებებზე ეს ამცირებს pipeline-ს ~12 წუთიდან ~3-მდე.

9. Docker compose named services-ით და nginx-ით

Docker compose — მარტივი, იკითხება და საკმარისი ჩვენი მასშტაბისთვის. ალტერნატივები (Kubernetes, Nomad, ECS) — ეს სხვა კლასი სირთულეა, და მათში ცხრა სერვისზე ერთ სერვერზე სარგებელს არ ვხედავ. Nginx ამთავრებს SSL-ს და ახდენს proxy-ს upstream-ებზე დომენების მიხედვით, healthcheck-ებით docker compose უზრუნველყოფს graceful restart-ს push-ების დროს.

10. NestJS modules + DI

NestJS — ეს, არსებითად, Angular-style დეკომპოზიციაა backend-ისთვის. თითოეული მოდული იზოლირებულია, დამოკიდებულებები გადაიცემა DI-ით. ეს იძლევა:

  • ადვილ ტესტირებას (თუმცა ამჟამად ტესტი არ არის — იხ. ტექ-ვალი);
  • დამოკიდებულებების ადვილ mock-ს გამართვის დროს;
  • ნათელ საზღვრებს: „orders დამოკიდებულია products-ზე და არა პირიქით".

ალტერნატივები (Express + ხელით აწყობილი სერვისები, Fastify + DIY DI) უფრო იაფია საწყის ღირებულებაში, მაგრამ უფრო ძვირი ოცდათექვსმეტი მოდულის მასშტაბზე.

ტექ-ვალი როგორც ინჟინერიის მომწიფებული პრაქტიკა

იდეალური პროექტი არ არსებობს. მომწიფებული გუნდი არ თამაშობს, რომ ყველაფერი სუფთა აქვს, არამედ აწარმოებს ღია ტექ-ვალის სიას და პრიორიტეტებს ანიჭებს. ჩემთან — იგივე.

Critical (დახურულია)

  • JWT access-token 30 დღე. დაფიქსდა აუდიტის შემდეგ: ამჟამად 15 წუთი, refresh-token-ები rotation-ით.
  • OTP-კოდი ლოგებში. წაიშალა console.log თვით კოდით auth.service.ts-ში.
  • Math.random პაროლებისთვის. შეცვლილია crypto.randomBytes-ით ხუთივე ადგილზე.
  • Inventory race condition. დაიხურა DB-ტრანზაქციით პირობით WHERE inventory >= qty.
  • PromoStatus enum DELETE → DELETED.
  • პროდუქტის შექმნისას duplicate variants.

High (მუშავდება)

  • Refresh token rotation-ის გარეშე — ნაწილობრივ დახურულია, საჭიროა წინა token-ის invalidation-ის დასრულება გამოყენების დროს.
  • Refresh endpoint rate limit-ის გარეშე — ერთადერთი auth-endpoint-ი @Throttle-ის გარეშე. მარტივი შესწორება, რიგში.
  • WhatsApp existsCache memory leak — Map eviction-სტრატეგიის გარეშე. საჭიროა LRU ან TTL eviction.
  • TypeORM synchronize ნაწილ მოდულებში — ვცვლი სუფთა მიგრაციებზე.

Medium (ტექ-ვალი დისციპლინისთვის)

  • ტესტი არ არის. ისინი იყვნენ, მაგრამ ადრეულ ეტაპებზე წაიშალა იტერაციების სიჩქარისთვის. ეს ხილული ვალია, და მე მას ღიად ვაღიარებ. გეგმა — დავამატო unit-ტესტები კრიტიკულ სერვისებზე (auth, orders, inventory) და e2e checkout-ზე.
  • Silent .catch(() => {}) orders/notifications-ში. სადღაც ეს განზრახულად არის (fire-and-forget ნოტიფიკაციებისთვის), მაგრამ orders-ში ეს ხურავს რეალურ შეცდომებს. საჭიროა ყოველი case-ის review.
  • Inventory log შეკვეთის დროს — ცხრილი არსებობს, მაგრამ ჩამოწერისას ჩაწერა ჯერ არ ხდება. ეს ხილული gap-ია audit-trail-ში.
  • Session TTL 30 დღე — ძალიან გრძელია security-მგრძნობიარე ანგარიშებისთვის. საჭიროა მინიმუმ 14 დღემდე შემცირება ან როლების შემოღება სხვადასხვა TTL-ით.

პრინციპები

ვიცავ რამდენიმე მარტივ წესს ტექ-ვალთან მუშაობისას:

  • ყოველი ვალი ჩაწერილია პრიორიტეტითა და კონტექსტით, რათა გუნდმა ის ხელში აიღოს.
  • Critical-ვალი იხურება შემდეგ release-მდე. „შემდეგ sprint-ში გამოვასწორებთ"-ი არ არის.
  • High და Medium მიდიან backlog-ში და პრიორიტეტდებიან feature-ებთან ერთად; უსაზღვრო გადადება არ შეიძლება.
  • ტექ-ვალი ღიად ჩანს: არ ვმალავ stakeholder-ებისგან private-tracker-ში. ეს გამჭვირვალე ინჟინერიის კულტურის ნაწილია.

დასკვნითი ფიქრები

თუ ერთ ხაზში შევაჯამებ: მარტივი ბლოკები, ნათელი საზღვრები, დაცული კომუნიკაციის წერტილები. პლატფორმა გადარჩება არა საოცარი ტექნოლოგიების ხარჯზე, არამედ იმის ხარჯზე, რომ ყოველი ფენა აკეთებს თავის სამუშაოს და სხვის საქმეში არ ერევა. API არაფერი იცის Telegram-ის შესახებ, ბოტი არაფერი იცის ბაზის შესახებ, admin არ იმეორებს frontend-ს, mini-app — ეს ცალკე SPA-ა სწორ ზედაპირზე (Telegram WebView). CI ხელახლა ააწყობს მხოლოდ შეცვლილს. Prod — ეს ცხრა docker-კონტეინერია nginx-ის უკან, და თითოეული მათგანი დამოუკიდებლად შეიძლება გადაიტვირთოს. Observability აგროვებს ივენტებსა და rrweb-სესიებს ზედმეტი ინფრასტრუქტურის გარეშე. უსაფრთხოება — რამდენიმე ფენაზე, და თითოეული მათგანი — მოკლე და გასაგები კოდი.

ეს სტეკი კარგად ჯდება „boring tech principle"-ზე: ვირჩევ შემოწმებულ ინსტრუმენტებს, ვმინიმუმაცემ ცვალებად ნაწილებს და ყურადღებით ვუყურებ რა არის მომწიფებული და რა — ჯერ არა. რაც უფრო ნაკლები მოულოდნელობებია ინფრასტრუქტურაში, მით უფრო მეტი დრო რჩება პროდუქტისთვის.

ბმულები

  • პროექტის ბიზნეს-გვერდი: Last-mile Delivery — case
  • Admin ფუნქციონალი: Platform admin
  • ეს განყოფილება source-ში: apps/api/src/modules/, apps/web/src/app/[locale]/, .github/workflows/docker-build.yml, docker-compose.prod.yml, package.json, turbo.json