# 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 · **Staging:** https://dev.sase.tr | **Repo:** `/home/s/sase.tr` Generated: 2026-03-02 · Last refreshed: 2026-05-24 --- ## 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 checkout + webhook) | | **Storage** | MinIO (S3-compatible) | | **Jobs** | BullMQ (Redis-backed queues) | | **Email** | Postal (transactional email) | | **Analytics** | PostHog (product analytics) | | **Observability** | OpenTelemetry + Sentry (API), Grafana Faro (frontend) | | **Testing** | Vitest 3, Playwright 1.50 | | **Linting** | Biome (2-space, double quotes, semicolons, trailing commas) | | **CI/CD** | GitHub Actions `qa-gate` (PR tests) · Coolify (dev.sase.tr) · GitHub Actions → PM2 (prod) | | **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) │ │ │ ├── instrument.ts # Sentry init (imported first in main.ts) │ │ │ ├── app.module.ts # Root module (global guards, interceptors, filters) │ │ │ ├── worker.ts # Standalone worker process │ │ │ ├── instrument-worker.ts # Sentry init for the 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 payment processing (checkout + webhook) │ │ │ ├── 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 │ │ │ ├── internal-admin/ # Founder-only "Supe Panel" (impersonation, lifecycle, billing, refunds) │ │ │ ├── analytics/ # OEM copy-event tracking │ │ │ ├── posthog/ # PostHog server-side event capture │ │ │ ├── blog/ # Blog posts (public + automation webhook) │ │ │ ├── changelog/ # Changelog entries (public + automation webhook) │ │ │ ├── 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 | | Sentry Init | `apps/api/src/instrument.ts` · `instrument-worker.ts` | Sentry instrumentation (imported before app bootstrap) | | 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 (checkout session + webhook), payment history | | **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, query logs, referral & daily stats, OEM copy logs | | **InternalAdminModule** | impersonation, lifecycle, billing, payments, vehicles (controller+service each) | Founder-only "Supe Panel": read-only impersonation, suspend/ban, trial/plan edits, refunds, vehicle cache ops. Gated by `InternalTokenGuard` (`INTERNAL_API_TOKEN`) | | **AnalyticsModule** | module, service, controller | OEM code copy-event tracking (`oemCodeCopies`) | | **PostHogModule** | module, service | Server-side PostHog event capture (optional) | | **BlogModule** | module, service, controller | Blog posts: public list/detail + automation webhook (Bearer token) | | **ChangelogModule** | module, service, controller, spec | Changelog entries: public list + admin CRUD + automation webhook | | **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) | | **Guard** | `ImpersonationReadonlyGuard` (global) | Blocks mutations during a read-only impersonation session | | **Guard** | `InternalTokenGuard` (per-route) | Validates `INTERNAL_API_TOKEN` bearer for `/api/internal/admin/*` (Supe Panel) | | **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, status (pending/completed/failed/refunded) ├── stripeSessionId, stripePaymentIntentId, adminNote ├── iyzicoPaymentId, bankAccountId → bankAccounts, eftReceiptUrl (@deprecated — legacy iyzico/EFT, kept for historical rows only) └── Indexes: userId, status, stripeSessionId bankAccounts └── (legacy) EFT bank-transfer accounts; retained for historical payments only 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 oemCodeCopies └── OEM code copy events (userId, oemCode, optional partId/vehicleId/categoryId) — admin copy-log analytics blogPosts └── SEO blog posts (slug, title, content, publish state) changelogEntries └── Product changelog entries (shown in Settings → Changelog) 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 | Create Stripe checkout session | | `POST` | `/api/payments/stripe/webhook` | Public | Stripe webhook callback | | `GET` | `/api/payments/me` | User | Payment history | > EFT (bank-transfer) endpoints were removed in the Stripe migration (FN-343). The `payments` table keeps `eftReceiptUrl`/`bankAccountId`/`iyzicoPaymentId` only for historical rows. #### 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 | |--------|------|------|-------------| | `GET` | `/api/vehicles/preview/:vin` | Public | Preview vehicle by VIN (pre-decode) | | `POST` | `/api/vehicles/decode` | User | Decode VIN (rate limited: 20/min) | | `GET` | `/api/vehicles/history` | User | Vehicle history (paginated) | | `POST` | `/api/vehicles/report-vin` | User | Report an unrecognized VIN to admin | | `GET` | `/api/vehicles/:vehicleId/prefetch-status` | User | Catalog prefetch job status | | `GET` | `/api/vehicles/:vehicleId/categories/:categoryId` | User | Parts for a vehicle + category | | `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) | **EMEX sub-catalog** (`/api/catalog/emex/*`): `brands`, `brands/:code/vehicles`, `brands/:code/wizard`, `brands/:code/wizard-vehicles`, `vehicles/:id/groups`, `vehicles/:id/groups/:groupId`, `search?oem=`, `match?catalogCode=&name=` **PCAT sub-catalog** (`/api/catalog/pcat/*`): `catalogs`, `catalogs/:id/models`, `catalogs/:id/groups`, `catalogs/:id/models/:modelId/cars`, `cars/:carId/groups`, `cars/:carId/groups/:groupId/schemas`, `schemas/:schemaImageId` #### 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 | |--------|------|------|-------------| | `POST` | `/api/admin/users` | Admin | Create a user | | `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/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) | | `GET` | `/api/admin/copy-logs` | Admin | OEM code copy logs | | `GET` | `/api/admin/copy-logs/top` | Admin | Top copied OEM codes (`?days=`) | #### Analytics | Method | Path | Auth | Description | |--------|------|------|-------------| | `POST` | `/api/analytics/oem-copy` | User | Track OEM code copy event (oemCode, partId?, vehicleId?, categoryId?) | #### Blog | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/blog/posts` | Public | List published posts | | `GET` | `/api/blog/posts/:slug` | Public | Get post by slug | | `POST` | `/api/blog/posts/internal` | Token (Bearer) | Create/update post via automation | #### Internal Admin — "Supe Panel" (founder-only, `InternalTokenGuard`) | Method | Path | Description | |--------|------|-------------| | `POST` | `/api/internal/admin/users/:id/impersonate-readonly` | Mint a signed read-only impersonation consume-URL | | `GET` | `/api/admin/impersonate/consume?t=` | Consume token → set read-only session cookie | | `POST` | `/api/internal/admin/users/:id/{suspend,reactivate,ban}` | Account lifecycle | | `POST` | `/api/internal/admin/subscriptions/:id/{trial/extend,activate,change-plan,cancel,resume,brands}` | Billing / subscription overrides | | `POST` | `/api/internal/admin/payments/:id/refund` | Refund a payment | | `POST` | `/api/internal/admin/vehicles/:vin/cache-clear` | Clear cached catalog data for a VIN | | `DELETE` | `/api/internal/admin/vehicles/:vin` | Force-delete a vehicle | #### 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`, account `status` (active/suspended/banned — non-active users are rejected by `AuthGuard`). **Internal/founder access:** the `/api/internal/admin/*` "Supe Panel" routes are gated by `InternalTokenGuard` (`INTERNAL_API_TOKEN` bearer), separate from user auth. Read-only impersonation issues an HMAC-signed consume token (`INTERNAL_IMPERSONATION_SECRET`); the resulting session is mutation-blocked by `ImpersonationReadonlyGuard`. --- ## Frontend Web ### Routes & Pages **Router:** TanStack Router (file-based, auto-generated route tree — 42 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 (Stripe checkout redirect) | | `/dashboard/billing` | `routes/dashboard/billing.tsx` | Payment history | | `/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) | > Source-specific sub-browsers also exist: `/dashboard/catalog/emex/$catalogCode[/$vehicleId]` (EMEX group navigation) and `/dashboard/catalog/pcat/$catalogId` (PartsCatalogs). A `/dashboard/service-test` page surfaces catalog-source status. #### 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/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 `