Files
sase.tr/docs/INDEX.md
Semih Yesilyurt 6283b66502
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
docs: refresh INDEX.md to match live code
Surgical refresh of the drifted project index: corrected the repo path,
documented the internal-admin "Supe Panel", payments now Stripe-only (EFT
history preserved as deprecated), CI/CD -> qa-gate + Coolify(dev)/PM2(prod),
added blog/changelog/posthog modules + Sentry entry points, route count
36 -> 42, and replaced the fictional docs list with the files that exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:34:49 +03:00

997 lines
52 KiB
Markdown

# 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 `<title>`, 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, 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` |
> **Staging / prod:** `docker-compose.coolify.yml` (repo root) defines the deployed services — `api`, `worker`, and `sase-redis` (Redis 7.4). PostgreSQL and MinIO are external/managed in those environments.
### 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 — `.github/workflows/qa-gate.yml`** (PR gate, 15min timeout):
- Triggers on PRs touching `apps/api/**` or `apps/web/**`; cancels superseded runs for the same PR.
- Detects which app changed, then runs `pnpm --filter <app> test` only for the changed app(s) (Node 22 + pnpm).
**Deploy — Coolify (staging) + GitHub Actions → PM2 (production):**
- **`dev` branch → Coolify → https://dev.sase.tr** via Gitea/GitHub webhook. The container is built from the root `Dockerfile`; runtime services are defined in `docker-compose.coolify.yml` (`api`, `worker`, `sase-redis`). Migrations run on container start via `apps/api/start.sh`, before the server boots.
- **`main` branch → production (https://sase.tr)** is promoted manually by the maintainer (merge `dev → main`), then shipped via GitHub Actions → SSH → PM2 (`pnpm build` + `pnpm db:migrate` + `pm2 reload`). See `AGENTS.md` for the dev-only deploy policy.
### 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 / Dir | Topic |
|------|-------|
| `docs/INDEX.md` | This file — comprehensive project reference |
| `README.md` | Quick stack overview & commands (Turkish) |
| `CLAUDE.md` | Claude Code project guide |
| `AGENTS.md` | Fusion agent deploy policy (dev-only; prod is human-merged) |
| `knowledge.md` | Long-form knowledge base (Dify.ai export) |
| `docs/pl24-catalog/*.md` | Per-brand PL24 catalog notes (BMW, Mercedes, Ford, …) + `_summary.md` |
| `docs/product/*.md` | CRO funnel audits & product shortlists |
| `docs/design-specs/*.md` | UX/design specs (e.g. VIN-decode error branches) |
| `docs/clarification/*.md` | Open requirement clarifications |
| `docs/00-testing.md` · `docs/TASK_REGISTRY_GUIDELINES.md` | Testing guide · task-registry conventions |
| `docs/analytics-queries.sql` | Saved analytics SQL |