# Sase.tr - Project Documentation Index > **Automotive parts search platform** for the Turkish market with VIN decoding, subscription-based access, and multi-source parts catalog integration. > **URL:** https://sase.tr | **Repo:** `/home/s/ss` Generated: 2026-03-02 --- ## Table of Contents - [Architecture Overview](#architecture-overview) - [Tech Stack](#tech-stack) - [Project Structure](#project-structure) - [Entry Points](#entry-points) - [Backend API](#backend-api) - [Modules & Services](#modules--services) - [Common Infrastructure](#common-infrastructure) - [Integrations](#integrations) - [Database Schema](#database-schema) - [Job Queues](#job-queues) - [Telemetry](#telemetry) - [API Endpoints](#api-endpoints) - [Authentication](#authentication) - [Frontend Web](#frontend-web) - [Routes & Pages](#routes--pages) - [Components](#components) - [Hooks & Stores](#hooks--stores) - [Lib Utilities](#lib-utilities) - [Internationalization](#internationalization) - [Shared Packages](#shared-packages) - [Testing](#testing) - [Infrastructure & Deployment](#infrastructure--deployment) - [Environment Variables](#environment-variables) - [Key Commands](#key-commands) - [Key Dependencies](#key-dependencies) - [Quick Start](#quick-start) --- ## Architecture Overview **Monorepo** powered by pnpm workspaces + Turborepo. ``` ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ Vite/React19 │────▶│ NestJS 10 │────▶│ PostgreSQL 17│ │ (port 3000) │ │ (port 4000) │ └──────────────┘ └─────────────┘ │ │────▶┌──────────────┐ │ │ │ Redis 7.4 │ └──────────────┘ └──────────────┘ │ ┌──────┴──────┐ │ Worker │────▶ BullMQ Queues │ (PM2 fork) │ (EMEX scrape, cron) └─────────────┘ ``` **Three runtime processes** managed by PM2: 1. `sase-api` — NestJS API server (fork mode) 2. `sase-web` — Vite/React frontend (fork mode) 3. `sase-worker` — Background job processor (fork mode) --- ## Tech Stack | Layer | Technology | |-------|-----------| | **Frontend** | Vite 6.3, React 19, TanStack Router 1.120, TypeScript 5.7 | | **Styling** | Tailwind CSS 4, shadcn/ui (Radix primitives) | | **State** | Zustand 5, TanStack React Query 5 | | **Backend** | NestJS 10.4, TypeScript | | **Database** | PostgreSQL 17, Drizzle ORM 0.41 | | **Cache** | Redis 7.4, ioredis | | **Auth** | Better Auth 1.2 (email/password + Google OAuth) | | **Payments** | Stripe (card), EFT (bank transfer with receipt upload) | | **Storage** | MinIO (S3-compatible) | | **Jobs** | BullMQ (Redis-backed queues) | | **Email** | Postal (transactional email) | | **Analytics** | PostHog (product analytics) | | **Observability** | OpenTelemetry (API), Grafana Faro (frontend) | | **Testing** | Vitest 3, Playwright 1.50 | | **Linting** | Biome (2-space, double quotes, semicolons, trailing commas) | | **CI/CD** | GitHub Actions → SSH deploy → PM2 | | **Package Mgmt** | pnpm 10.29, Turborepo 2 | --- ## Project Structure ``` sase.tr/ ├── apps/ │ ├── api/ # NestJS backend API │ │ ├── src/ │ │ │ ├── main.ts # Bootstrap (global prefix /api, CORS, Helmet, rate limiting) │ │ │ ├── app.module.ts # Root module (global guards, interceptors, filters) │ │ │ ├── worker.ts # Standalone worker process │ │ │ ├── health.controller.ts # Health check endpoint │ │ │ ├── auth/ # Better Auth integration │ │ │ ├── users/ # User account management │ │ │ ├── brands/ # Brand CRUD │ │ │ ├── plans/ # Pricing plan CRUD │ │ │ ├── subscriptions/ # Subscription lifecycle │ │ │ ├── payments/ # Stripe + EFT payment processing │ │ │ ├── referrals/ # Referral program │ │ │ ├── vehicles/ # VIN decoding + vehicle history │ │ │ ├── categories/ # Parts category tree │ │ │ ├── parts/ # Auto parts catalog │ │ │ ├── catalog/ # VIN-less catalog browser (PL24 model families) │ │ │ ├── translations/ # Automotive term translations │ │ │ ├── admin/ # Admin dashboard endpoints │ │ │ ├── analytics/ # Usage analytics tracking │ │ │ ├── common/ # Shared guards, pipes, interceptors, filters, decorators │ │ │ ├── config/ # Runtime configuration │ │ │ ├── database/ # Drizzle ORM setup + schemas (core, emex, pl24, parts-catalogs, relations) │ │ │ ├── redis/ # Redis client module │ │ │ ├── storage/ # MinIO/S3 service │ │ │ ├── email/ # Postal email service │ │ │ ├── jobs/ # BullMQ queues + processors │ │ │ ├── telemetry/ # OpenTelemetry SDK (tracing, metrics) │ │ │ └── integrations/ # External API integrations │ │ │ ├── corgi/ # Offline VIN WMI decoder │ │ │ ├── pl24/ # PL24 parts catalog API + parsers │ │ │ ├── parts-catalogs/ # PartsCatalogs API (groups, parts, auth) │ │ │ ├── emex/ # EMEX scraper (Playwright) │ │ │ └── vin-api/ # NHTSA VIN API fallback │ │ ├── drizzle.config.ts │ │ └── vitest.config.ts │ │ │ └── web/ # Vite + React frontend │ └── src/ │ ├── main.tsx # Entry point (RouterProvider, QueryClient, Faro, PostHog) │ ├── routeTree.gen.ts # Auto-generated TanStack route tree (DO NOT EDIT) │ ├── routes/ # TanStack Router file-based routes │ │ ├── __root.tsx # Root layout (theme, toaster, PostHog tracking) │ │ ├── index.tsx # Landing page │ │ ├── _auth.tsx # Auth layout (login, register, etc.) │ │ ├── dashboard.tsx # Dashboard layout (protected) │ │ └── dashboard/ # Dashboard subroutes (catalog, vehicles, admin, etc.) │ ├── components/ # React components │ │ ├── admin/ # DailyChart │ │ ├── schema/ # SchemaViewer, HotspotOverlay, PartsPanel, SchemaToolbar │ │ ├── subscription/ # BrandSelector │ │ ├── vehicles/ # VehicleCard, VinInput │ │ ├── categories/ # CategoryTree, CategoryGrid │ │ ├── payment/ # PaymentContent │ │ └── settings/ # SettingsContent │ ├── hooks/ # useAuth, useParts, useSchemaInteraction │ ├── lib/ # api-client, auth-client, i18n, posthog, faro, toast, user-settings, category-icons │ ├── stores/ # Zustand stores (auth, schema) │ └── messages/ # i18n JSON (tr.json, en.json) │ ├── packages/ │ ├── shared/ # @sase/shared — types, Zod schemas, constants, utils │ ├── config/ # @sase/config — Zod env validation │ └── ui/ # @sase/ui — shadcn/Radix component library │ ├── docker/ # Docker Compose (PostgreSQL, Redis, MinIO, nginx) ├── scripts/ # Build/deployment/debug scripts ├── .github/workflows/ # CI (lint, typecheck, test, build) + Deploy (SSH, PM2) ├── ecosystem.config.js # PM2 process configuration ├── turbo.json # Turborepo task pipeline ├── biome.json # Linter/formatter config └── pnpm-workspace.yaml # Workspace root ``` --- ## Entry Points | Process | Path | Description | |---------|------|-------------| | API Server | `apps/api/src/main.ts` | NestJS bootstrap (Helmet, CORS, rate limiting) | | Root Module | `apps/api/src/app.module.ts` | Global guards, interceptors, filters | | Worker | `apps/api/src/worker.ts` | BullMQ background job processor | | Health | `apps/api/src/health.controller.ts` | Health check endpoint | | Frontend | `apps/web/src/main.tsx` | React 19 + TanStack Router + Query + Faro + PostHog | | Root Layout | `apps/web/src/routes/__root.tsx` | Theme, Toaster, PostHog tracking | --- ## Backend API ### Modules & Services | Module | Files | Purpose | |--------|-------|---------| | **AuthModule** | module, service, controller, auth.ts | Better Auth (email/password + Google OAuth) | | **UsersModule** | module, service, controller, spec | Profile CRUD, password change, account deletion, OAuth connections | | **BrandsModule** | module, service, controller, spec | Brand CRUD (cached, admin-managed) | | **PlansModule** | module, service, controller, spec | Pricing plan CRUD (cached, admin-managed) | | **SubscriptionsModule** | module, service, controller, spec | Create, activate, cancel, resume, extend subscriptions | | **PaymentsModule** | module, service, controller, spec | Stripe card payments, EFT with receipt upload, admin approval | | **ReferralsModule** | module, service, controller, spec | Referral code generation, application, tier-based rewards | | **VehiclesModule** | module, service, controller, spec | VIN decode (multi-source fallback), vehicle history, brand access check | | **CategoriesModule** | module, service, controller, spec | Hierarchical category tree, schema pictures | | **PartsModule** | module, service, controller, spec | Parts by category, OEM code search | | **CatalogModule** | module, service, controller, dto | VIN-less PL24 catalog browser: brands, models, category trees, parts | | **TranslationsModule** | module, service, controller, spec | Automotive term translation (Redis → DB → Dictionary fallback) | | **AdminModule** | module, service, controller, spec | Dashboard stats, user management, payment approval, analytics | | **AnalyticsModule** | module, service, controller | Usage analytics tracking | | **EmailModule** | module, service | Postal transactional emails (password reset, welcome, payment confirmation) | | **StorageModule** | module, service | S3/MinIO file upload/download | | **RedisModule** | module, service, provider | Key-value cache operations | ### Common Infrastructure | Type | Name | Behavior | |------|------|----------| | **Guard** | `AuthGuard` (global) | Validates Better Auth session; skip with `@Public()` | | **Guard** | `RolesGuard` (global) | Checks `@Roles("admin")` metadata against `user.role` | | **Guard** | `BrandAccessGuard` (per-route) | Verifies user's subscription includes the target brand | | **Guard** | `ThrottlerGuard` (global) | Rate limiting (100/min default) | | **Interceptor** | `TransformInterceptor` (global) | Wraps responses: `{success: true, data: ...}` | | **Interceptor** | `LoggingInterceptor` (global) | Logs method, URL, status, response time | | **Interceptor** | `TimeoutInterceptor` (global) | 30s request timeout | | **Pipe** | `VinValidationPipe` (per-route) | Validates VIN: 17 chars, alphanumeric, no I/O/Q | | **Filter** | `HttpExceptionFilter` (global) | Returns `{success: false, error: {code, message}}` | | **Filter** | `DrizzleExceptionFilter` (global) | Catches unique constraint violations → 409 Conflict | | **Middleware** | `FileUploadValidation` | PNG/JPG/PDF only, max 5MB | **Custom Decorators:** - `@Public()` — Skip authentication - `@CurrentUser(field?)` — Inject authenticated user (or specific field) - `@Roles(...roles)` — Require role(s) - `@ThrottleAuth()` — 5 req/min - `@ThrottleVinDecode()` — 20 req/min - `@ThrottleGeneral()` — 100 req/min **Shared DTOs:** `ApiResponseDto`, `PaginationDto` ### Integrations **VIN Decode Fallback Chain:** Corgi (offline WMI) → **PartsCatalogs API** → PL24 API → EMEX Scraper → NHTSA VIN API **Category Fetch Fallback Chain (per source):** DB cache → PL24 → PartsCatalogs → EMEX (triggered by `getCategoryTree`) | Integration | Type | Path | Notes | |-------------|------|------|-------| | **Corgi** | Offline DB | `corgi/` | WMI database for brand identification (+ spec) | | **PL24** | REST API | `pl24/` | Multi-brand catalog API + auth + parsers (BMW, Mercedes, Generic, Ford Legacy) | | **PartsCatalogs** | REST API | `parts-catalogs/` | Multi-brand catalog API. Files: service, auth-service, module, types. Supports fetchGroups, fetchParts with parameterized car queries | | **EMEX** | Browser scraper | `emex/` | Playwright-based (emexdwc.ae), async via BullMQ. Files: service, browser, mapper, types | | **VIN-API** | REST API | `vin-api/` | NHTSA VIN decoder (last-resort fallback) | | **Stripe** | Payment API | — | Global payment processor for card payments | | **MinIO** | S3 API | — | Receipt uploads, schema images | ### Database Schema **ORM:** Drizzle ORM 0.41 with PostgreSQL **Schema files:** `apps/api/src/database/schema/` - `core.ts` — Main application tables - `emex.ts` — EMEX scraper cache tables - `pl24.ts` — PL24 catalog cache tables - `parts-catalogs.ts` — PartsCatalogs API cache tables - `relations.ts` — Drizzle ORM relationships #### Core Tables ``` users ├── id (uuid, PK) ├── name, email (unique), emailVerified, image ├── role (default: "user"), referralCode (unique), referredBy └── createdAt, updatedAt sessions / accounts / verifications └── Better Auth managed tables brands ├── id (uuid, PK), name, slug (unique), logoUrl, isActive └── createdAt, updatedAt plans ├── id (uuid, PK), name, brandCount ├── priceMonthly, priceYearly, isActive └── createdAt, updatedAt userSubscriptions ├── id (uuid, PK), userId → users, planId → plans ├── status (pending/active/cancelled/expired) ├── billingPeriod (monthly/yearly), startDate, endDate, cancelledAt └── Indexes: userId, status userBrands (junction) ├── userId → users, subscriptionId → userSubscriptions, brandId → brands └── Unique: (userId, subscriptionId, brandId) payments ├── id (uuid, PK), userId → users, subscriptionId → userSubscriptions ├── amount, currency, method (stripe/eft), status (pending/completed/failed/refunded) ├── stripePaymentIntentId, eftReceiptUrl, adminNote └── Indexes: userId, status vehicles ├── id (uuid, PK), vin (unique), brandId → brands ├── brandName, model, year, engine, transmission, bodyType, market ├── rawData (jsonb), source └── Indexes: vin Note: Shared across users; access via userVehicles junction table userVehicles (junction) ├── userId → users, vehicleId → vehicles ├── lastAccessedAt └── Unique: (userId, vehicleId) categories ├── id (uuid, PK), vehicleId → vehicles (nullable), catalogVehicleId → catalogVehicles (nullable) ├── name, nameOriginal, parentId (self-ref), externalId, source └── Indexes: vehicleId, parentId Note: DUAL FK — exactly one of vehicleId (VIN-based) or catalogVehicleId (VIN-less) is non-null per row parts ├── id (uuid, PK), vehicleId → vehicles (nullable), catalogVehicleId → catalogVehicles (nullable) ├── categoryId → categories, oemCode, name, nameOriginal, description, quantity, position, hotspotIndex └── Indexes: vehicleId, categoryId, oemCode Note: DUAL FK — same pattern as categories schemaPics ├── id (uuid, PK), categoryId → categories ├── imageUrl, hotspots (jsonb), source └── Indexes: categoryId queryLogs ├── id (uuid, PK), userId → users, vin, brandId → brands ├── source, success, errorMessage, responseTimeMs └── Indexes: (userId, createdAt), vin referrals ├── id (uuid, PK), referrerId → users, referredId → users (unique) ├── rewardApplied, createdAt └── Indexes: referrerId, referredId passwordResetTokens └── id, userId → users, token (unique), expiresAt, usedAt emexCategoryTranslations └── id, originalName (unique), translatedName, isManual catalogVehicles ├── id (uuid, PK), source (pl24), serviceName, brandName, brandId → brands ├── model, year, engine, bodyType, transmission, market ├── serviceVehicleId, catalogPath, architecture (P5_MODERN/P4_LEGACY) ├── metadata (jsonb), categoriesFetched (bool) └── Unique: (serviceName, serviceVehicleId) Note: categories and parts tables both have catalogVehicleId FK (nullable) for VIN-less catalog data ``` #### Integration Tables - `pl24_*` — PL24 catalog cache (catalogs, vehicles, VINs, part groups, parts, schemas) - `emex_*` — EMEX scraper cache (similar structure with translations) - `parts_catalogs_*` — PartsCatalogs API cache (defined in `parts-catalogs.ts`) ### Migration Workflow **Strategy:** Formal Drizzle SQL migrations with a custom hash-based runner. The migration runner at `apps/api/src/database/migrate.ts` uses a **custom hash-based runner** instead of Drizzle's built-in `migrate()`. It tracks applied migrations by SHA256 of SQL file content rather than by timestamp comparison. **Why custom?** Drizzle's built-in `migrate()` compares `folderMillis` timestamps from `drizzle/meta/_journal.json`. When `drizzle-kit generate` produces incorrect timestamps (clock skew bug), new migrations can be silently skipped. Hash-based tracking is immune to clock skew and provides deterministic idempotency. **How it works:** 1. Reads all previously-applied migration hashes from `drizzle.__drizzle_migrations` 2. Reads `drizzle/meta/_journal.json` for the list of migration files in index order 3. For each migration file, computes SHA256 of the SQL content 4. If the hash already exists in `__drizzle_migrations` → skip 5. If the hash is new → execute all statements in a single transaction, then record the hash **Idempotent baseline:** `0000_brief_guardian.sql` uses `IF NOT EXISTS` for all `CREATE TABLE`, `CREATE INDEX`, and `CREATE UNIQUE INDEX` statements. `ALTER TABLE ADD CONSTRAINT` statements are wrapped in `DO $$ BEGIN ... EXCEPTION WHEN duplicate_object ... END $$` blocks. This allows the baseline to be safely re-applied on existing databases where `__drizzle_migrations` was bootstrapped to a different hash. **Schema change workflow:** 1. Edit schema files in `apps/api/src/database/schema/` 2. Run `pnpm db:generate` — produces a timestamped `.sql` file in `apps/api/drizzle/` 3. Review the SQL diff in PR (the migration is version-controlled alongside code) 4. Commit and deploy — migration runs automatically on container startup **Container startup:** The API server runs `apps/api/start.sh` which executes `node dist/database/migrate.js` BEFORE starting the NestJS server. This ensures all pending migrations are applied before any code runs. **Bootstrap note:** Existing databases with no migration history (`__drizzle_migrations` table empty or missing) are handled automatically. The runner creates the tracking table and applies the baseline migration idempotently (all statements use `IF NOT EXISTS` guards, so no errors on re-application). This is safe for databases previously managed via `pnpm db:push`. **Manual commands:** - `pnpm db:migrate` — Run migrations in dev (uses `tsx` for direct TS execution) - `pnpm db:migrate:dist` — Run migrations against compiled JS (for production manual override) - `pnpm db:generate` — Create a new migration from schema changes - `pnpm db:push` — Direct schema push (still available for local dev, discouraged for shared environments) **Worker process:** The BullMQ worker overrides the Docker CMD in `docker-compose.coolify.yml` and does NOT run migrations. Only the API service handles migrations, preventing race conditions. ### Job Queues **Framework:** BullMQ with Redis | Queue | Trigger | Schedule | Action | |-------|---------|----------|--------| | `EMEX_SCRAPE` | On-demand (VIN decode) | — | Scrapes EMEX via Playwright, stores results | | `CATALOG_PREFETCH` | On-demand (after VIN decode) | — | Prefetches category tree + parts for all sources (depth-limited, rate-limited, cooldown-guarded). Handled by `PrefetchWorkerService`. | | `SUBSCRIPTION_EXPIRY` | Cron | Daily 3:00 AM | Expires ended subscriptions, removes brand access | | `QUERY_CLEANUP` | Cron | Weekly Sun 4:00 AM | Cleans old query log entries | **Files:** `apps/api/src/jobs/` — `jobs.module.ts`, `bull.config.ts`, `processors/`, `queues/`, `prefetch-worker.service.ts`, `prefetch-utils.ts`, `prefetch.types.ts` ### Telemetry **Location:** `apps/api/src/telemetry/` OpenTelemetry SDK with: - `sdk-factory.ts` — SDK initialization - `tracing.ts` — Distributed tracing (API) - `worker-tracing.ts` — Worker process tracing - `metrics.ts` — Prometheus metrics - `__tests__/telemetry.spec.ts` — Tests Instrumentation: Express, HTTP, ioredis, NestJS Core, BullMQ, Drizzle ORM ### API Endpoints **Global prefix:** `/api` | **Rate limit:** 100 req/min (default) #### Authentication | Method | Path | Auth | Description | |--------|------|------|-------------| | `*` | `/api/auth/**` | Public | Better Auth catch-all (register, login, logout, OAuth, session) | #### Health | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/health` | Public | Health check (`{status: "ok", timestamp}`) | #### Users | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/users/me` | User | Get current user profile | | `PATCH` | `/api/users/me` | User | Update profile (name, image) | | `POST` | `/api/users/me/change-password` | User | Change password | | `DELETE` | `/api/users/me` | User | Delete account | | `GET` | `/api/users/me/connections` | User | List OAuth connections | | `DELETE` | `/api/users/me/connections/:provider` | User | Unlink OAuth provider | | `GET` | `/api/users` | Admin | List all users (paginated) | | `GET` | `/api/users/:id` | Admin | Get user by ID | #### Brands | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/brands` | Public | List all brands (cached 30 min) | | `POST` | `/api/brands` | Admin | Create brand | | `PATCH` | `/api/brands/:id` | Admin | Update brand | #### Plans | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/plans` | Public | List all plans (cached 30 min) | | `POST` | `/api/plans` | Admin | Create plan | | `PATCH` | `/api/plans/:id` | Admin | Update plan | #### Subscriptions | Method | Path | Auth | Description | |--------|------|------|-------------| | `POST` | `/api/subscriptions` | User | Create subscription with brand selection | | `GET` | `/api/subscriptions/me` | User | Get active subscription | | `PATCH` | `/api/subscriptions/cancel` | User | Cancel subscription | | `PATCH` | `/api/subscriptions/resume` | User | Resume cancelled subscription | | `GET` | `/api/subscriptions` | Admin | List all subscriptions (paginated) | #### Payments | Method | Path | Auth | Description | |--------|------|------|-------------| | `POST` | `/api/payments/stripe/checkout` | User | Start Stripe card payment | | `POST` | `/api/payments/stripe/webhook` | Public | Stripe webhook callback | | `POST` | `/api/payments/eft` | User | Create EFT payment | | `POST` | `/api/payments/eft/:id/receipt` | User | Upload EFT receipt (PNG/JPG/PDF, 5MB max) | | `PATCH` | `/api/payments/eft/:id/approve` | Admin | Approve EFT payment | | `PATCH` | `/api/payments/eft/:id/reject` | Admin | Reject EFT payment | | `GET` | `/api/payments/me` | User | Payment history | | `GET` | `/api/payments/pending` | Admin | Pending EFT payments | #### Referrals | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/referrals/stats` | User | Referral count + reward tier status | | `GET` | `/api/referrals/me` | User | Referral code + list of referees | | `POST` | `/api/referrals/apply` | User | Apply a referral code | #### Vehicles | Method | Path | Auth | Description | |--------|------|------|-------------| | `POST` | `/api/vehicles/decode` | User | Decode VIN (rate limited: 20/min) | | `GET` | `/api/vehicles/history` | User | Vehicle history (paginated) | | `GET` | `/api/vehicles/:id` | User | Get vehicle details | | `DELETE` | `/api/vehicles/:id` | User | Delete vehicle | #### Categories & Parts | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/categories/tree/:vehicleId` | User | Category tree for vehicle (cached) | | `GET` | `/api/categories/:id` | User | Category with schema pictures | | `GET` | `/api/parts/category/:categoryId` | User | Parts by category | | `GET` | `/api/parts/search?oem=` | User | Search parts by OEM code | | `GET` | `/api/parts/:id` | User | Part by ID | #### Catalog (VIN-less Browser) | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/catalog/brands` | User | List PL24 brands with access flags (hasAccess per subscription) | | `GET` | `/api/catalog/brands/:brandName/catalogs` | User | Service catalog list for a brand (multi-catalog brands like Ford) | | `GET` | `/api/catalog/brands/:brandName/models` | User | Model list for a brand (`?service=` optional) — DB cache → PL24 fetch | | `GET` | `/api/catalog/vehicles/:id` | User | Get a catalog vehicle by ID | | `GET` | `/api/catalog/vehicles/:id/ford-config` | User | Ford model-year/engine/gearbox options (Legacy Ford brands) | | `GET` | `/api/catalog/vehicles/:id/psa-bodies` | User | PSA body style options (Citroën/Peugeot) | | `GET` | `/api/catalog/vehicles/:id/psa-engines` | User | PSA engine options for a given body (`?body=`) | | `GET` | `/api/catalog/vehicles/:id/psa-gearboxes` | User | PSA gearbox options for a given body+engine (`?body=&engine=`) | | `GET` | `/api/catalog/vehicles/:id/categories` | User | Category tree (`?body=&engine=&gearbox=`); Redis cached 2h | | `GET` | `/api/catalog/vehicles/:id/categories/:categoryId` | User | Sub-categories or parts+schema (`?body=&engine=&gearbox=`); lazy PL24 fetch | | `POST` | `/api/catalog/explore/:serviceName` | Admin | Explore PL24 service structure (discovery tool) | #### Translations | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/translations/:key` | Public | Get translation | | `POST` | `/api/translations/batch` | User | Batch translate items | | `PUT` | `/api/translations/:key` | Admin | Set/override translation | | `GET` | `/api/translations/search?q=` | Admin | Search translations | #### Admin | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/admin/dashboard` | Admin | Dashboard stats (users, revenue, queries) | | `GET` | `/api/admin/users` | Admin | User list with search (paginated) | | `GET` | `/api/admin/users/:id` | Admin | User detail with subscriptions & payments | | `GET` | `/api/admin/payments/pending` | Admin | Pending EFT payments | | `GET` | `/api/admin/query-logs` | Admin | Query logs (paginated, filterable) | | `GET` | `/api/admin/referrals` | Admin | Referral stats (paginated) | | `GET` | `/api/admin/stats/daily` | Admin | Daily VIN decode stats (last 30 days) | #### Analytics | Method | Path | Auth | Description | |--------|------|------|-------------| | `POST` | `/api/analytics/oem-copy` | User | Track OEM code copy event (oemCode, partId?, vehicleId?, categoryId?) | #### Changelog | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/changelog` | Public | List changelog entries (Redis-cached, 30min) | | `POST` | `/api/changelog` | Admin | Create changelog entry | | `POST` | `/api/changelog/internal` | Token (Bearer) | Create changelog entry (Fusion automation webhook, CHANGELOG_AUTOMATION_TOKEN env) | | `PATCH` | `/api/changelog/:id` | Admin | Update changelog entry | | `DELETE` | `/api/changelog/:id` | Admin | Delete changelog entry | ### Authentication **Provider:** Better Auth 1.2 ``` Flow: 1. Client sends credentials to /api/auth/sign-in/email (or /sign-up, /oauth) 2. Better Auth validates, creates session in DB 3. Returns session cookie (better-auth.session_token, 5-min cache) 4. Subsequent requests include cookie automatically 5. AuthGuard calls auth.api.getSession() to validate 6. User injected into request via @CurrentUser() decorator ``` **User model extensions:** `role` (default: "user"), `referralCode`, `referredBy` --- ## Frontend Web ### Routes & Pages **Router:** TanStack Router (file-based, auto-generated route tree — 36 files) #### Public | Path | Route File | Description | |------|------------|-------------| | `/` | `routes/index.tsx` | Landing page with VIN decode + features + pricing CTA | | `/pricing` | `routes/pricing.tsx` | Plan comparison (1/2/3 brand, full package) | | `/about` | `routes/about.tsx` | About page | | `/contact` | `routes/contact.tsx` | Contact page | | `/blog` | `routes/blog.tsx` | Blog listing page | | `/blog/:slug` | `routes/blog_/$slug.tsx` | Blog post detail (SEO pre-rendered) | | `/demo` | `routes/demo.tsx` | Demo page | | `/privacy` | `routes/privacy.tsx` | Privacy policy | | `/terms` | `routes/terms.tsx` | Terms of service | | `/kvkk` | `routes/kvkk.tsx` | KVKK (Turkish data protection) | #### Auth (Layout: `_auth.tsx`) | Path | Route File | Description | |------|------------|-------------| | `/login` | `routes/_auth/login.tsx` | Email/password + Google OAuth | | `/register` | `routes/_auth/register.tsx` | Account creation | | `/forgot-password` | `routes/_auth/forgot-password.tsx` | Email-based recovery | | `/reset-password` | `routes/_auth/reset-password.tsx` | Token-based password reset | #### Dashboard (Protected, Layout: `dashboard.tsx`) | Path | Route File | Description | |------|------------|-------------| | `/dashboard` | `routes/dashboard/index.tsx` | Dashboard home | | `/dashboard/search` | `routes/dashboard/search.tsx` | VIN Search — main VIN decoder input | | `/dashboard/history` | `routes/dashboard/history.tsx` | Past VIN decode searches | | `/dashboard/subscription` | `routes/dashboard/subscription/index.tsx` | Plan selection & brand picker | | `/dashboard/subscription/pay` | `routes/dashboard/subscription/pay.tsx` | Card (Stripe) or EFT payment | | `/dashboard/billing` | `routes/dashboard/billing.tsx` | Payment history & receipts | | `/dashboard/settings` | `routes/dashboard/settings.tsx` | Profile, Security, Connections, Referral, Account, Changelog tabs | | `/dashboard/vehicles/$id` | `routes/dashboard/vehicles_/$id/index.tsx` | Vehicle details | | `/dashboard/vehicles/$id/categories/$categoryId` | `routes/dashboard/vehicles_/$id/categories_/$categoryId.tsx` | Interactive schema + parts table | #### Catalog Browser (VIN-less, Protected) | Path | Route File | Description | |------|------------|-------------| | `/dashboard/catalog` | `routes/dashboard/catalog/index.tsx` | Brand grid with hasAccess flags; locked brands show upgrade CTA | | `/dashboard/catalog/$brandName` | `routes/dashboard/catalog_/$brandName/index.tsx` | Model list for brand (fetched from PL24) | | `/dashboard/catalog/$brandName/$modelId` | `routes/dashboard/catalog_/$brandName_/$modelId/index.tsx` | Vehicle details + category tree (grid/tree toggle) | | `/dashboard/catalog/$brandName/$modelId/categories/$categoryId` | `routes/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId.tsx` | Category sub-groups or schema+parts (lazy PL24 fetch) | #### Admin (Role-based) | Path | Route File | Description | |------|------------|-------------| | `/dashboard/admin` | `routes/dashboard/admin/index.tsx` | Stats overview with charts | | `/dashboard/admin/users` | `routes/dashboard/admin/users.tsx` | User management | | `/dashboard/admin/payments` | `routes/dashboard/admin/payments.tsx` | EFT approval workflow | | `/dashboard/admin/referrals` | `routes/dashboard/admin/referrals.tsx` | Referral program tracking | | `/dashboard/admin/analytics` | `routes/dashboard/admin/analytics.tsx` | Daily query statistics | | `/dashboard/admin/copy-logs` | `routes/dashboard/admin/copy-logs.tsx` | OEM code copy tracking | ### Components | Component | Location | Description | |-----------|----------|-------------| | **SchemaViewer** | `components/schema/schema-viewer.tsx` | Interactive parts diagram with zoom/pan/fullscreen | | **HotspotOverlay** | `components/schema/hotspot-overlay.tsx` | SVG overlay for clickable part regions | | **PartsPanel** | `components/schema/parts-panel.tsx` | Parts table with OEM codes, synced with schema | | **SchemaToolbar** | `components/schema/schema-toolbar.tsx` | Zoom/reset/fullscreen controls | | **BrandSelector** | `components/subscription/brand-selector.tsx` | Brand grid with plan-enforced max selection | | **VehicleCard** | `components/vehicles/vehicle-card.tsx` | Vehicle info card (VIN, brand, model, year) | | **VehicleSelectModal** | `components/vehicles/vehicle-select-modal.tsx` | Modal for selecting a vehicle from history (used during VIN decode flow) | | **VinInput** | `components/vehicles/vin-input.tsx` | VIN entry input component | | **CategoryTree** | `components/categories/category-tree.tsx` | Recursive expandable category hierarchy | | **CategoryGrid** | `components/categories/category-grid.tsx` | Grid layout for category browsing | | **DailyChart** | `components/admin/daily-chart.tsx` | Daily VIN decode stats chart | | **PaymentContent** | `components/payment/payment-content.tsx` | Payment form and flow | | **SettingsContent** | `components/settings/settings-content.tsx` | User settings panel content | | **ChangelogTab** | `components/settings/changelog-tab.tsx` | Timeline + accordion changelog viewer | | **PsaVariantSelector** | `components/catalog/psa-variant-selector.tsx` | PSA (Citroën/Peugeot) body/engine/gearbox picker for VIN-less catalog | | **FordVariantSelector** | `components/catalog/ford-variant-selector.tsx` | Ford model-year/engine/gearbox picker for VIN-less catalog | **UI primitives** from `@sase/ui`: Button, Card, Input, Label, Badge (with stage variant), Dialog, Tabs, Separator, Skeleton, Accordion ### Hooks & Stores **Hooks:** | Hook | Purpose | |------|---------| | `useAuth()` | Auth state + Better Auth client (`user`, `isAdmin`, `signIn`, `signUp`, `signOut`, `session`) | | `useCategoryParts(vehicleId, categoryId)` | TanStack Query for schema + parts + hotspots | | `useSchemaInteraction()` | Pan/zoom/pinch event handlers for schema viewer | | `usePageMeta(options)` | Sets ``, meta description, canonical, OG/Twitter tags; restores defaults on unmount | | `useChangelog()` | TanStack Query for changelog entries (staleTime: 30min) | **Stores (Zustand):** | Store | State | |-------|-------| | `useAuthStore()` | `{ user: User | null, isLoading }` | | `useSchemaStore()` | `{ highlightedGroup, selectedGroup, zoom, panX, panY, isFullscreen }` | **Data Fetching:** Custom `ApiClient` class (`lib/api-client.ts`) - Base URL: `/api` (proxied via Vite dev server) - Methods: `get<T>`, `post<T>`, `patch<T>`, `delete<T>`, `upload<T>` - Credentials: cookies (automatic) - Caching: TanStack React Query 5 (`staleTime: 60s`, `retry: 1`, no refetch on focus) ### Lib Utilities | File | Purpose | |------|---------| | `api-client.ts` | HTTP client wrapper with Faro error integration | | `auth-client.ts` | Better Auth client (signIn, signUp, signOut, useSession) | | `i18n.ts` | Zustand-based i18n (tr/en), `useTranslation()` hook | | `posthog.ts` | PostHog analytics initialization and tracking | | `faro.ts` | Grafana Faro frontend observability | | `user-settings.ts` | Theme/settings persistence | | `toast.ts` | Toast notification export | | `category-icons.ts` | Category icon mappings | ### Internationalization **Implementation:** Custom Zustand store with JSON message files | Locale | File | Coverage | |--------|------|----------| | Turkish (default) | `messages/tr.json` | Full | | English | `messages/en.json` | Full | **Usage:** `const { t, locale, setLocale } = useTranslation()` → `t("nav.search")` **Persistence:** `localStorage("sase-locale")`, updates `document.documentElement.lang` --- ## Shared Packages ### @sase/shared (`packages/shared/src/`) | Directory | Exports | |-----------|---------| | `types/` | User, UserProfile, UserSubscriptionSummary, Vehicle, VinDecodeResult, CategoryNode, VehicleSource, Brand, Plan, Subscription, UserBrand, CreateSubscriptionInput, SubscriptionStatus, Payment, PaymentMethod, PaymentStatus, EftPaymentInput, Part, PartSource, PartSearchResult, Category, CategoryWithSchema, SchemaPic, Hotspot, ApiResponse, ApiError, PaginationMeta, PaginationInput, PaginatedResult, ChangelogEntry, CreateChangelogEntry, UpdateChangelogEntry, ChangelogChangeType | | `schemas/` | loginSchema, registerSchema, forgotPasswordSchema, resetPasswordSchema, vinSchema, paginationSchema, changelogEntrySchema, createChangelogEntrySchema, updateChangelogEntrySchema, changelogChangeTypeEnum (Zod) | | `constants/` | ERROR_CODES (30+, prefixed AUTH/VIN/SUB/PAY), PLANS (Single/Double/Triple/Full), REFERRAL_REWARDS (Tier 1: 3→7d, Tier 2: 5→30d), VIN_REGEX, EMAIL_REGEX, OEM_CODE_REGEX, CURRENCY | | `utils/` | VIN validator (check digit, WMI extraction, model year decode), currency (formatTRY, kurus↔lira), formatters (VIN, date, datetime, Turkish slug, referral code) | **Dependency:** `zod ^3.24.0` ### @sase/config (`packages/config/src/index.ts`) Zod env schema exporting `envSchema`, `Env` type, `validateEnv()`. Groups: DATABASE_URL, REDIS_*, BETTER_AUTH_*, GOOGLE_*, MINIO_*, CORS_ORIGIN, STRIPE_*, PL24_*, EMEX_*, ML_PREDICTION_ENABLED, POSTAL_*, OTEL_* ### @sase/ui (`packages/ui/src/`) Components: Button (CVA variants), Input, Card (6 compound parts), Badge (CVA, includes stage variant: alpha/beta/prod), Label, Skeleton, Separator, Dialog (10 compound parts), Tabs (4 compound parts), Accordion (4 compound parts), `cn()` utility Dependencies: Radix UI (accordion, dialog, dropdown-menu, label, popover, select, separator, slot, tabs, tooltip), class-variance-authority, clsx, tailwind-merge, lucide-react --- ## Testing ### Unit Tests (Vitest 3) | Category | Files | Total Lines | |----------|-------|-------------| | **Service specs** | 12 (admin, brands, categories, changelog, parts, payments, plans, referrals, subscriptions, translations, users, vehicles) | 2,521 | | **Guard specs** | 3 (auth, roles, brand-access) | 380 | | **Pipe specs** | 1 (vin-validation) | 77 | | **Integration specs** | 1 (corgi) | 121 | | **Telemetry specs** | 1 | 97 | | **Total API** | **18 test files** | **3,196 lines** | **Test Config:** - API: `apps/api/vitest.config.ts` — `src/**/*.spec.ts`, v8 coverage (text + lcov) - Web: `apps/web/vitest.config.ts` — `src/**/*.{test,spec}.{ts,tsx}`, jsdom, `@/` alias, test-setup (jest-dom) **Test Pattern:** Mock Drizzle DB with chainable query builder, `vi.mock()` for external deps ### Playwright 1.50 - Installed at root level (`package.json`) - Used by EMEX integration (`emex.browser.ts`) for web scraping - Used by PartsCatalogs auth service for JWT capture via Playwright - Test scripts in `scripts/` (vin-e2e-test.js) --- ## Infrastructure & Deployment ### Docker Compose (Development) | Service | Image | Port | Volume | |---------|-------|------|--------| | PostgreSQL 17 | `postgres:17-alpine` | 127.0.0.1:5432 | `pg_data` | | Redis 7.4 | `redis:7.4-alpine` | 127.0.0.1:6379 | `redis_data` | | MinIO | `minio/minio` | 9000 (API), 9001 (Console) | `minio_data` | ### SEO Infrastructure - `apps/web/scripts/prerender.mjs` — Pre-renders public pages to static HTML (landing, blog posts, pricing, etc.) for crawler/bot visibility - `apps/web/public/robots.txt` — Crawl directives with sitemap reference - `apps/web/public/sitemap.xml` — Static sitemap for public pages - `apps/web/index.html` — Contains default OG/Twitter meta tags; `usePageMeta()` overrides at runtime per page ### Nginx (`docker/nginx/sites/`) - `sase.tr.conf` — Frontend SPA + `/api` proxy + `/collect/` Faro telemetry CORS proxy + gzip (level 6) + 1-year asset cache + security headers - `api.sase.tr.conf` — NestJS proxy (60s timeout for VIN decode) + SSL + blocked paths (.git, .env, node_modules) ### CI/CD (GitHub Actions) **`ci.yml`** — Runs on all branches & PRs to main (15min timeout): 1. Biome lint 2. TypeScript type check 3. Vitest unit tests 4. Full build **`deploy.yml`** — Runs on push to `main` (10min timeout): 1. SSH into production 2. `git pull origin main` 3. `pnpm install` 4. `pnpm build` 5. `pnpm db:migrate` (migrations) 6. PM2 reload all ### PM2 Configuration | Process | Command | Port | Memory | |---------|---------|------|--------| | `sase-api` | `pnpm dev` (dev) / `dist/main.js` (prod) | 4000 | 512MB | | `sase-web` | `pnpm dev` (dev) / serve dist (prod) | 3000 | 512MB | | `sase-worker` | `dist/worker.js` | — | 256MB | --- ## Environment Variables ### Required | Variable | Description | |----------|-------------| | `DATABASE_URL` | PostgreSQL connection string | | `REDIS_PASSWORD` | Redis authentication | | `BETTER_AUTH_SECRET` | Auth encryption key (min 32 chars) | | `BETTER_AUTH_URL` | Auth service URL | | `MINIO_ENDPOINT` | MinIO/S3 endpoint | | `MINIO_ACCESS_KEY` | MinIO access key | | `MINIO_SECRET_KEY` | MinIO secret key | | `MINIO_PUBLIC_URL` | Public URL for stored files | | `CORS_ORIGIN` | Allowed origins | ### Optional | Variable | Default | Description | |----------|---------|-------------| | `PORT` | 4000 | API server port | | `REDIS_HOST` | 127.0.0.1 | Redis host | | `REDIS_PORT` | 6379 | Redis port | | `MINIO_BUCKET_NAME` | sase-schemas | Storage bucket | | `MINIO_USE_SSL` | false | HTTPS for MinIO | | `GOOGLE_CLIENT_ID` | — | Google OAuth client ID | | `GOOGLE_CLIENT_SECRET` | — | Google OAuth secret | | `STRIPE_SECRET_KEY` | — | Stripe payment secret key | | `STRIPE_PUBLISHABLE_KEY` | — | Stripe publishable key | | `PL24_BASE_URL` | — | PL24 catalog API URL | | `PL24_COMPANY_CODE` | — | PL24 company code | | `PL24_USERNAME` | — | PL24 credentials | | `PL24_PASSWORD` | — | PL24 credentials | | `EMEX_USERNAME` | — | EMEX scraper credentials | | `EMEX_PASSWORD` | — | EMEX scraper credentials | | `ML_PREDICTION_ENABLED` | false | Enable ML predictions | | `POSTAL_API_URL` | — | Postal email service URL | | `POSTAL_API_KEY` | — | Postal API key | | `POSTAL_FROM_ADDRESS` | noreply@sase.tr | Sender email | | `POSTAL_FROM_NAME` | Sase.tr | Sender name | | `OTEL_ENABLED` | false | Enable OpenTelemetry | | `OTEL_EXPORTER_OTLP_ENDPOINT` | — | OTLP collector endpoint | | `OTEL_EXPORTER_OTLP_HEADERS` | — | OTLP auth headers | | `OTEL_SERVICE_NAME` | sase-api | OTel service name | | `OTEL_TRACE_SAMPLE_RATE` | 1.0 | Trace sampling rate (0-1) | Full schema: `packages/config/src/index.ts` --- ## Key Commands ```bash # Development pnpm dev # Start all apps (Turbo) pnpm dev --filter=api # API only pnpm dev --filter=web # Web only # Build & Quality pnpm build # Build all packages + apps pnpm lint # Biome lint check pnpm typecheck # TypeScript --noEmit pnpm test # Run all tests (Vitest) # Database (run from apps/api/) pnpm db:push # Push schema to DB (Drizzle) pnpm db:studio # Open Drizzle Studio pnpm db:seed # Seed database pnpm db:generate # Generate migration # Route generation pnpm --filter web exec tsr generate # Production pm2 start ecosystem.config.js pm2 reload all ``` --- ## Key Dependencies | Package | Version | Purpose | |---------|---------|---------| | NestJS | 10.4 | Backend framework | | Drizzle ORM | 0.41 | Database ORM | | Better Auth | 1.2 | Cookie-based auth | | React | 19 | Frontend UI | | TanStack Router | 1.120 | File-based routing | | TanStack Query | 5 | Server state management | | Zustand | 5 | Client state management | | Tailwind CSS | 4 | Styling | | Vite | 6.3 | Build tool | | Vitest | 3.x | Unit testing | | Playwright | 1.50 | Browser automation | | BullMQ | 5.30 | Job queues | | Biome | latest | Linting/formatting | | Turborepo | 2.x | Monorepo orchestration | | OpenTelemetry | 0.212 | Backend observability | | Grafana Faro | 2.2 | Frontend observability | | PostHog | latest | Product analytics | --- ## Quick Start 1. `docker compose -f docker/docker-compose.yml up -d` — Start PostgreSQL, Redis, MinIO 2. `cp apps/api/.env.example apps/api/.env` — Configure env vars 3. `pnpm install` — Install dependencies 4. `pnpm --filter api db:push && pnpm --filter api db:seed` — Setup database 5. `pnpm dev` — Start all services 6. Open `http://localhost:3000` — Frontend 7. Admin login: `admin@sase.tr` / `Sase2026` --- ## Pricing Model | Plan | Brands | Monthly | Yearly | |------|--------|---------|--------| | 1 Marka | 1 | 200 TRY | 2,000 TRY | | 2 Marka | 2 | 350 TRY | 3,500 TRY | | 3 Marka | 3 | 500 TRY | 5,000 TRY | | Full Paket | Unlimited | 999 TRY | 9,990 TRY | **Referral Rewards:** - Tier 1: 3 referrals → 7-day subscription extension - Tier 2: 5 referrals → 30-day subscription extension --- ## Documentation | File | Topic | |------|-------| | `docs/INDEX.md` | This file — comprehensive project reference | | `CLAUDE.md` | Claude Code project guide | | `.claude/product-marketing-context.md` | Marketing context | | `docs/00-overview.md` — `docs/13-analytics-posthog.md` | Detailed topic docs |