# SASE v2 — Claude Code Project Guide > VIN/chassis number lookup + auto parts catalog platform for the Turkish market. > **URL:** https://sase.tr | **Repo root:** `/home/s/ss` ## Tech Stack | Layer | Technology | |-------|-----------| | **Monorepo** | pnpm 10.29 workspaces + Turborepo | | **Backend** | NestJS 10.4, TypeScript 5.7, Node 22 | | **Database** | PostgreSQL 17 + Drizzle ORM 0.41 | | **Cache/Queue** | Redis 7.4 + ioredis + BullMQ | | **Auth** | Better Auth 1.2 (cookie-based sessions) | | **Frontend** | Vite 6.3, React 19, TanStack Router 1.120, TanStack Query 5 | | **Styling** | Tailwind CSS 4, shadcn/ui (Radix primitives) | | **State** | Zustand 5 | | **Payments** | Iyzico (card) + EFT (bank transfer) | | **Email** | Postal (transactional) | | **Storage** | MinIO (S3-compatible) | | **Analytics** | PostHog (product analytics) | | **Observability** | OpenTelemetry (API) + Grafana Faro (frontend) | | **Testing** | Vitest 3, Playwright 1.50 | | **Linting** | Biome (2-space indent, double quotes, semicolons, trailing commas) | | **CI/CD** | GitHub Actions → SSH deploy → PM2 | ## Project Structure ``` ss/ ├── apps/ │ ├── api/ # NestJS backend (port 4000, prefix /api) │ │ └── src/ │ │ ├── main.ts # Bootstrap (Helmet, CORS, rate limiting) │ │ ├── app.module.ts # Root module (global guards/interceptors/filters) │ │ ├── worker.ts # BullMQ worker process │ │ ├── database/schema/ # Drizzle ORM schemas (core.ts, emex.ts, pl24.ts, parts-catalogs.ts, relations.ts) │ │ ├── common/ # Guards, interceptors, filters, decorators, pipes, DTOs │ │ ├── integrations/ # corgi/, pl24/, emex/, parts-catalogs/, vin-api/ │ │ └── [modules]/ # auth, users, brands, plans, subscriptions, payments, │ │ # referrals, vehicles, categories, parts, catalog, │ │ # translations, analytics, admin, jobs, email, storage, redis, │ │ # telemetry │ └── web/ # Vite + React frontend (port 3000) │ └── src/ │ ├── main.tsx # Entry (RouterProvider, QueryClientProvider, Faro, PostHog) │ ├── routes/ # TanStack Router file-based routes │ ├── components/ # admin/, schema/, vehicles/, categories/, payment/, settings/, subscription/ │ ├── hooks/ # useAuth, useParts, useSchemaInteraction │ ├── stores/ # Zustand: auth.store.ts, schema.store.ts │ ├── lib/ # api-client, auth-client, i18n, posthog, faro, toast, user-settings, category-icons │ └── messages/ # tr.json, en.json (i18n) ├── packages/ │ ├── shared/ # @sase/shared — types, Zod schemas, constants, utils │ ├── config/ # @sase/config — Zod env validation schema │ └── ui/ # @sase/ui — shadcn-based React components ├── docker/ # docker-compose.yml (PostgreSQL, Redis, MinIO) + nginx configs ├── scripts/ # deploy.sh, test/debug scripts ├── docs/ # INDEX.md + detailed docs (00-13) └── ecosystem.config.js # PM2 config (api, worker) ``` ## Key Commands ```bash pnpm dev # Start all apps (Turbo) pnpm build # Build all packages + apps pnpm test # Run all tests (Vitest) pnpm lint # Biome lint check pnpm typecheck # TypeScript --noEmit # 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 # Single app pnpm dev --filter=api # API only pnpm dev --filter=web # Web only # Route generation (auto on dev/build, manual if needed) pnpm --filter web exec tsr generate ``` ## Coding Conventions - **Formatter:** Biome — 2-space indent, 100-char line width, double quotes, semicolons, trailing commas - **Module pattern:** NestJS feature modules — each domain has `module.ts`, `service.ts`, `controller.ts`, `*.dto.ts`, `*.spec.ts` - **API response format:** All responses wrapped in `{ success: true, data: ... }` via `TransformInterceptor` - **Error format:** `{ success: false, error: { code, message } }` via `HttpExceptionFilter` - **Auth:** Cookie-based via Better Auth. Skip with `@Public()` decorator. Get user with `@CurrentUser()`. - **Roles:** `@Roles("admin")` decorator + global `RolesGuard` - **Validation:** Zod schemas in `@sase/shared`, imported by both API and Web - **DB naming:** snake_case columns, camelCase in TypeScript (Drizzle mapping) - **Frontend aliases:** `@/` maps to `apps/web/src/` in Vite. `@sase/*` maps to `packages/*/src` in tsconfig. - **Route files:** TanStack Router auto-generates `routeTree.gen.ts` — never edit manually - **i18n:** Turkish default (`tr.json`), English available (`en.json`). Use `useTranslation()` hook → `t("key")` ## Backend Modules | Module | Purpose | |--------|---------| | **AuthModule** | Better Auth (email/password + Google OAuth) | | **UsersModule** | Profile CRUD, password change, OAuth connections, account deletion | | **BrandsModule** | Brand CRUD (cached, admin-managed) | | **PlansModule** | Pricing plan CRUD (cached, admin-managed) | | **SubscriptionsModule** | Create, activate, cancel, resume, extend subscriptions | | **PaymentsModule** | Iyzico card + EFT with receipt upload + admin approval | | **ReferralsModule** | Referral code generation, tier-based rewards | | **VehiclesModule** | VIN decode (multi-source fallback), vehicle history, brand access check | | **CategoriesModule** | Hierarchical category tree, schema pictures | | **PartsModule** | Parts by category, OEM code search | | **CatalogModule** | VIN-less catalog browser — PL24 brands, models, category trees, parts | | **TranslationsModule** | Automotive term translation (Redis → DB → Dictionary fallback) | | **AnalyticsModule** | OEM code copy tracking, usage analytics | | **AdminModule** | Dashboard stats, user management, payment approval | | **EmailModule** | Postal transactional emails (welcome, payment confirmation, password reset) | | **StorageModule** | S3/MinIO file upload/download | | **RedisModule** | Key-value cache operations | | **JobsModule** | BullMQ queues + processors + prefetch worker | | **TelemetryModule** | OpenTelemetry SDK (tracing, metrics) | ## Architecture Patterns - **Global guards order:** ThrottlerGuard → AuthGuard → RolesGuard - **Global interceptors:** TransformInterceptor → LoggingInterceptor → TimeoutInterceptor (30s) - **Global filters:** HttpExceptionFilter, DrizzleExceptionFilter (unique constraint → 409) - **Vite proxy:** `/api` requests → `http://localhost:4000` in dev ### VIN Decode Fallback Chain Corgi (offline WMI) → **PartsCatalogs API** → PL24 API → EMEX scraper → NHTSA VIN API > Note: PartsCatalogs was added between Corgi and PL24 as it has broader VIN coverage. If multiple car matches return, the frontend prompts the user to select. ### Category Fetch Chain (VIN-based) DB cache → PL24 → PartsCatalogs → EMEX (lazy, source-based in `getCategoryTree`) ### Catalog Browse Flow (VIN-less) 1. `GET /catalog/brands` → check user subscription access per brand 2. `GET /catalog/brands/:name/models` → PL24 `fetchVehicleList()` → stored in `catalogVehicles` table 3. `GET /catalog/vehicles/:id/categories` → PL24 `fetchMainGroups()` → stored in `categories` with `catalogVehicleId` 4. `GET /catalog/vehicles/:id/categories/:categoryId` → PL24 `fetchSubGroupsByPath()` / `fetchPartsByPath()` → stored lazily ### PL24 Catalog Architectures - **P5_MODERN** (REST JSON API): VW Group, BMW, Mini, Mercedes, Porsche, Renault, Dacia, Alpine, Jaguar, Land Rover, Toyota, Lexus, MAN, Mitsubishi, Suzuki, etc. - **LEGACY_PSA** (HTML scraping): Citroën, Peugeot - **LEGACY_FORD** (HTML scraping): Ford passenger (wf0_parts) + commercial (fordt_parts) - **LEGACY_HYUNDAI_KIA** (HTML scraping): Hyundai, Kia - **LEGACY_NISSAN** (HTML scraping): Nissan, Infiniti - **LEGACY_OPEL** (HTML scraping): Opel, Vauxhall - **LEGACY_VOLVO** (HTML scraping): Volvo, Polestar Our PL24 account (tr-903645) supports **VAG group only** for VIN-less catalog. Other brands may return errors on model listing. All P4 Legacy VIN decodes route through `PL24FordLegacyService.decodeVinForService(vin, serviceName)`. ### Caching Strategy - Redis: VIN decode results (24h), category trees (2h for catalog browser), parts fetches (1h), translations - HTTP Cache: brands, plans (30min `Cache-Control`) - Sessions: Better Auth (5min Redis) ### Job Queues (BullMQ) | Queue | Trigger | Schedule | |-------|---------|----------| | `EMEX_SCRAPE` | On-demand (VIN decode) | — | | `CATALOG_PREFETCH` | After VIN decode | — (depth-limited, rate-limited, cooldown-guarded) | | `SUBSCRIPTION_EXPIRY` | Cron | Daily 3:00 AM | | `QUERY_CLEANUP` | Cron | Weekly Sunday 4:00 AM | ### Subscription & Access Control - Plans have `brandCount` field: `0` = unlimited access, `N` = limited to N brands - Brand access tracked in `userBrands` junction table (userId + subscriptionId + brandId) - Full plan (brandCount=0) auto-adds all active brands on activation - `BrandAccessGuard` (per-route) verifies user's subscription includes the requested brand ## Database Schema Summary **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 **Key tables in `core.ts`:** - `users`, `sessions`, `accounts`, `verifications` — Better Auth managed - `brands`, `plans` — Catalog of available brands/plans (admin-managed) - `userSubscriptions` — status: pending/active/trial/cancelled/expired - `userBrands` — junction table controlling brand access per subscription - `payments` — Iyzico or EFT, status tracking - `vehicles` — one per unique VIN, shared across users via `userVehicles` - `userVehicles` — junction (userId + vehicleId unique), tracks lastAccessedAt - `categories` — parent-child hierarchy; has both `vehicleId` (VIN-based) and `catalogVehicleId` (VIN-less) FKs (nullable for the other mode) - `parts` — OEM code, quantity, hotspot index; same dual FK pattern as categories - `schemaPics` — exploded view images + hotspots JSONB, linked to categories - `catalogVehicles` — VIN-less catalog: one record per PL24 service vehicle (unique on source + serviceVehicleId) - `queryLogs` — VIN decode audit trail - `oemCodeCopies` — OEM code copy events (analytics) - `referrals`, `passwordResetTokens`, `emexCategoryTranslations` ## Integrations | Integration | Type | Path | Notes | |-------------|------|------|-------| | **Corgi** | Offline DB | `integrations/corgi/` | WMI database for brand ID | | **PL24** | REST API + HTML scraper | `integrations/pl24/` | Multi-brand catalog; P5 (REST) + P4 Legacy (HTML). Services: `pl24.service.ts`, `pl24-ford-legacy.service.ts` | | **PartsCatalogs** | REST API + Playwright JWT | `integrations/parts-catalogs/` | Broad VIN coverage; JWT captured via Playwright from partner sites; IP-bound via DataImpulse proxy | | **EMEX** | Browser scraper | `integrations/emex/` | Playwright-based (emexdwc.ae), async via BullMQ | | **VIN-API** | REST API | `integrations/vin-api/` | NHTSA VIN decoder (last-resort fallback) | ## Frontend Routes **Public:** `/`, `/pricing`, `/about`, `/contact`, `/blog`, `/demo`, `/privacy`, `/terms`, `/kvkk` **Auth (layout `_auth.tsx`):** `/login`, `/register`, `/forgot-password`, `/reset-password` **Dashboard (protected, layout `dashboard.tsx`):** - `/dashboard` — Home - `/dashboard/search` — VIN decode input - `/dashboard/history` — Past VIN searches - `/dashboard/subscription` — Plan/brand selection - `/dashboard/subscription/pay` — Payment (Iyzico or EFT) - `/dashboard/billing` — Payment history - `/dashboard/settings` — Profile, Security, Connections, Referral tabs - `/dashboard/vehicles/$id` — Vehicle details - `/dashboard/vehicles/$id/categories/$categoryId` — Interactive schema + parts **Catalog Browser (VIN-less, protected):** - `/dashboard/catalog` — Brand grid with access flags - `/dashboard/catalog/$brandName` — Model list (from PL24) - `/dashboard/catalog/$brandName/$modelId` — Category tree/grid view - `/dashboard/catalog/$brandName/$modelId/categories/$categoryId` — Sub-categories or schema+parts **Admin (role-based):** - `/dashboard/admin` — Stats + charts - `/dashboard/admin/users` — User management - `/dashboard/admin/payments` — EFT approval workflow - `/dashboard/admin/referrals` — Referral tracking - `/dashboard/admin/analytics` — Daily query stats - `/dashboard/admin/copy-logs` — OEM code copy tracking ## Auth & Test Credentials - **Admin:** `admin@sase.tr` / `Sase2026` - **Login:** `POST /api/auth/sign-in/email` (returns session cookie) - **Cookie:** `better-auth.session_token` (or `__Secure-` prefix with HTTPS) - **Test VIN:** VW `WVWZZZ1JZ3W597935` ## Environment Variables **Required:** `DATABASE_URL`, `REDIS_PASSWORD`, `BETTER_AUTH_SECRET` (min 32 chars), `BETTER_AUTH_URL`, `MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, `MINIO_PUBLIC_URL`, `CORS_ORIGIN` **Optional (grouped):** - `PORT` (4000), `REDIS_HOST` (127.0.0.1), `REDIS_PORT` (6379), `MINIO_BUCKET_NAME` (sase-schemas), `MINIO_USE_SSL` (false) - `GOOGLE_CLIENT_ID/SECRET` — Google OAuth - `IYZICO_API_KEY/SECRET_KEY/BASE_URL` — Payment processing - `PL24_BASE_URL/COMPANY_CODE/USERNAME/PASSWORD` — PL24 catalog API - `EMEX_USERNAME/PASSWORD` — EMEX scraper - `PCAT_USE_PROXY` (true), `PCAT_PROXY_HOST` (gw.dataimpulse.com), `PCAT_PROXY_USER/PASS` — PartsCatalogs proxy - `POSTAL_API_URL/API_KEY`, `POSTAL_FROM_ADDRESS` (noreply@sase.tr), `POSTAL_FROM_NAME` (Sase.tr) — Email - `OTEL_ENABLED` (false), `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME` (sase-api), `OTEL_TRACE_SAMPLE_RATE` (1.0) — OpenTelemetry - `ML_PREDICTION_ENABLED` (false) Full schema: `packages/config/src/index.ts` ## Common Gotchas - **Frontend is Vite + TanStack Router** — NOT Next.js - **Categories & Parts tables have dual FKs:** `vehicleId` (VIN-based, nullable) and `catalogVehicleId` (VIN-less, nullable) — always check which context you're in - **PartsCatalogs JWT is IP-bound** via DataImpulse proxy; the auth service manages a warm pool of JWTs using Playwright - **P4 Legacy brands have no REST API** — Ford, PSA, Hyundai/Kia, Nissan, Opel, Volvo all use HTML scraping through `PL24FordLegacyService` - **`catalogVehicles` unique key** is `(source, serviceVehicleId)` — not by VIN (these vehicles may not have VINs) - **`routeTree.gen.ts`** is auto-generated by TanStack Router — never edit manually - **DrizzleExceptionFilter** catches unique constraint violations → 409 Conflict - **File uploads:** PNG/JPG/PDF only, max 5MB (middleware in `main.ts`) - **Env validation** uses Zod from `@sase/config` — app won't start if env vars invalid - **Playwright (EMEX/PartsCatalogs):** `waitUntil: 'networkidle'` (not `networkidle2`); `page.context().cookies()` (not `page.cookies()`) - **VIN decode may return candidates** if PartsCatalogs finds multiple matches — frontend shows selection modal ## ast-grep — Structural Code Search & Refactoring ast-grep (`sg`) does AST-aware pattern matching — finds code by structure, not text. Unlike `grep`, it understands syntax so `foo( bar )` and `foo(bar)` both match the pattern `foo($X)`. **Install:** `npm i -g @ast-grep/cli` (already installed globally) ### Pattern syntax | Syntax | Meaning | |--------|---------| | `$VAR` | Matches any **single** AST node (expression, identifier, etc.) | | `$$$ARGS` | Matches **zero or more** nodes (variadic — use for argument lists, statements) | | Literal code | Matches exact syntax structure | ### Common commands ```bash # Search by pattern in TypeScript files ast-grep -p 'console.log($$$)' -l ts apps/ # Search and preview rewrite (no changes yet) ast-grep -p '$A && $A()' --rewrite '$A?.()' -l ts apps/ # Interactive rewrite — confirm each change ast-grep -p '$A && $A()' --rewrite '$A?.()' --interactive -l ts apps/ # Apply all rewrites without confirmation ast-grep -p '$A && $A()' --rewrite '$A?.()' --update-all -l ts apps/ # Output matches as JSON (useful for scripting) ast-grep -p 'useQuery($$$)' -l tsx --json apps/web/src/ # Show surrounding context lines ast-grep -p 'db.select()' -l ts -C 3 apps/api/src/ # Limit to specific file globs ast-grep -p '@Public()' -l ts --globs 'apps/api/src/**/*.controller.ts' . ``` ### Language flags for this project | Flag | Use for | |------|---------| | `-l ts` | API services, guards, modules, DTOs | | `-l tsx` | React components, route files | | `-l json` | i18n message files | ### Useful patterns for this codebase ```bash # Find all @Public() decorated endpoints ast-grep -p '@Public()' -l ts apps/api/src/ # Find all useQuery calls (TanStack Query) ast-grep -p 'useQuery({$$$})' -l tsx apps/web/src/ # Find Drizzle inserts ast-grep -p 'db.insert($TABLE).values($$$)' -l ts apps/api/src/ # Find all Redis cache sets ast-grep -p 'this.redis.set($$$)' -l ts apps/api/src/ # Find t() translation calls missing a key ast-grep -p 't($KEY)' -l tsx apps/web/src/ # Find all BullMQ queue.add() calls ast-grep -p '$QUEUE.add($$$)' -l ts apps/api/src/jobs/ ``` ## Documentation - **Project index:** `docs/INDEX.md` (comprehensive — routes, API endpoints, DB schema, components) - **Marketing context:** `.claude/product-marketing-context.md` - **Detailed docs:** `docs/00-overview.md` through `docs/13-analytics-posthog.md` - **Memory:** `.claude/projects/-home-s-ss/memory/MEMORY.md`