Files
sase.tr/docs/INDEX.md
Fusion 561b02b3d2 feat(FN-343): remove lingering iyzico references after Stripe migration (+1 more)
Commits merged:
- chore(FN-343): remove lingering iyzico references from docs, config, and scripts
- feat(FN-343): remove lingering iyzico references after Stripe migration

Files changed:
CLAUDE.md                                    | 10 +++++-----
 README.md                                    |  2 +-
 apps/api/src/database/schema/core.ts         |  1 +
 apps/web/src/messages/en.json                |  1 -
 apps/web/src/messages/tr.json                |  1 -
 apps/web/src/routes/dashboard/billing.tsx    |  6 +++---
 docker-compose.coolify.yml                   |  5 ++---
 docs/INDEX.md                                | 27 ++++++++++++-------------
 knowledge.md                                 | 30 ++++++++++++++--------------
 packages/shared/src/constants/error-codes.ts |  1 -
 packages/shared/src/index.ts                 |  1 -
 packages/shared/src/types/payment.ts         | 12 +----------
 scripts/fn342-pw-verify.mjs                  |  2 +-
 scripts/validate-env.sh                      |  3 +--
 14 files changed, 43 insertions(+), 59 deletions(-)

Fusion-Task-Id: FN-343
2026-05-14 02:31:42 +00:00

46 KiB

Sase.tr - Project Documentation Index

Automotive parts search platform for the Turkish market with VIN decoding, subscription-based access, and multi-source parts catalog integration. URL: https://sase.tr | Repo: /home/s/ss

Generated: 2026-03-02


Table of Contents


Architecture Overview

Monorepo powered by pnpm workspaces + Turborepo.

┌─────────────┐     ┌──────────────┐     ┌──────────────┐
│ Vite/React19 │────▶│  NestJS 10   │────▶│ PostgreSQL 17│
│  (port 3000) │     │  (port 4000) │     └──────────────┘
└─────────────┘     │              │────▶┌──────────────┐
                     │              │     │  Redis 7.4   │
                     └──────────────┘     └──────────────┘
                            │
                     ┌──────┴──────┐
                     │   Worker    │────▶ BullMQ Queues
                     │  (PM2 fork) │     (EMEX scrape, cron)
                     └─────────────┘

Three runtime processes managed by PM2:

  1. sase-api — NestJS API server (fork mode)
  2. sase-web — Vite/React frontend (fork mode)
  3. sase-worker — Background job processor (fork mode)

Tech Stack

Layer Technology
Frontend Vite 6.3, React 19, TanStack Router 1.120, TypeScript 5.7
Styling Tailwind CSS 4, shadcn/ui (Radix primitives)
State Zustand 5, TanStack React Query 5
Backend NestJS 10.4, TypeScript
Database PostgreSQL 17, Drizzle ORM 0.41
Cache Redis 7.4, ioredis
Auth Better Auth 1.2 (email/password + Google OAuth)
Payments Stripe (card), EFT (bank transfer with receipt upload)
Storage MinIO (S3-compatible)
Jobs BullMQ (Redis-backed queues)
Email Postal (transactional email)
Analytics PostHog (product analytics)
Observability OpenTelemetry (API), Grafana Faro (frontend)
Testing Vitest 3, Playwright 1.50
Linting Biome (2-space, double quotes, semicolons, trailing commas)
CI/CD GitHub Actions → SSH deploy → PM2
Package Mgmt pnpm 10.29, Turborepo 2

Project Structure

sase.tr/
├── apps/
│   ├── api/                    # NestJS backend API
│   │   ├── src/
│   │   │   ├── main.ts                  # Bootstrap (global prefix /api, CORS, Helmet, rate limiting)
│   │   │   ├── app.module.ts            # Root module (global guards, interceptors, filters)
│   │   │   ├── worker.ts               # Standalone worker process
│   │   │   ├── health.controller.ts     # Health check endpoint
│   │   │   ├── auth/                    # Better Auth integration
│   │   │   ├── users/                   # User account management
│   │   │   ├── brands/                  # Brand CRUD
│   │   │   ├── plans/                   # Pricing plan CRUD
│   │   │   ├── subscriptions/           # Subscription lifecycle
│   │   │   ├── payments/                # Stripe + EFT payment processing
│   │   │   ├── referrals/               # Referral program
│   │   │   ├── vehicles/                # VIN decoding + vehicle history
│   │   │   ├── categories/              # Parts category tree
│   │   │   ├── parts/                   # Auto parts catalog
│   │   │   ├── catalog/                 # VIN-less catalog browser (PL24 model families)
│   │   │   ├── translations/            # Automotive term translations
│   │   │   ├── admin/                   # Admin dashboard endpoints
│   │   │   ├── analytics/               # Usage analytics tracking
│   │   │   ├── common/                  # Shared guards, pipes, interceptors, filters, decorators
│   │   │   ├── config/                  # Runtime configuration
│   │   │   ├── database/                # Drizzle ORM setup + schemas (core, emex, pl24, parts-catalogs, relations)
│   │   │   ├── redis/                   # Redis client module
│   │   │   ├── storage/                 # MinIO/S3 service
│   │   │   ├── email/                   # Postal email service
│   │   │   ├── jobs/                    # BullMQ queues + processors
│   │   │   ├── telemetry/              # OpenTelemetry SDK (tracing, metrics)
│   │   │   └── integrations/            # External API integrations
│   │   │       ├── corgi/               #   Offline VIN WMI decoder
│   │   │       ├── pl24/                #   PL24 parts catalog API + parsers
│   │   │       ├── parts-catalogs/      #   PartsCatalogs API (groups, parts, auth)
│   │   │       ├── emex/                #   EMEX scraper (Playwright)
│   │   │       └── vin-api/             #   NHTSA VIN API fallback
│   │   ├── drizzle.config.ts
│   │   └── vitest.config.ts
│   │
│   └── web/                    # Vite + React frontend
│       └── src/
│           ├── main.tsx                 # Entry point (RouterProvider, QueryClient, Faro, PostHog)
│           ├── routeTree.gen.ts         # Auto-generated TanStack route tree (DO NOT EDIT)
│           ├── routes/                  # TanStack Router file-based routes
│           │   ├── __root.tsx           # Root layout (theme, toaster, PostHog tracking)
│           │   ├── index.tsx            # Landing page
│           │   ├── _auth.tsx            # Auth layout (login, register, etc.)
│           │   ├── dashboard.tsx        # Dashboard layout (protected)
│           │   └── dashboard/           # Dashboard subroutes (catalog, vehicles, admin, etc.)
│           ├── components/              # React components
│           │   ├── admin/               #   DailyChart
│           │   ├── schema/              #   SchemaViewer, HotspotOverlay, PartsPanel, SchemaToolbar
│           │   ├── subscription/        #   BrandSelector
│           │   ├── vehicles/            #   VehicleCard, VinInput
│           │   ├── categories/          #   CategoryTree, CategoryGrid
│           │   ├── payment/             #   PaymentContent
│           │   └── settings/            #   SettingsContent
│           ├── hooks/                   # useAuth, useParts, useSchemaInteraction
│           ├── lib/                     # api-client, auth-client, i18n, posthog, faro, toast, user-settings, category-icons
│           ├── stores/                  # Zustand stores (auth, schema)
│           └── messages/                # i18n JSON (tr.json, en.json)
│
├── packages/
│   ├── shared/                 # @sase/shared — types, Zod schemas, constants, utils
│   ├── config/                 # @sase/config — Zod env validation
│   └── ui/                     # @sase/ui — shadcn/Radix component library
│
├── docker/                     # Docker Compose (PostgreSQL, Redis, MinIO, nginx)
├── scripts/                    # Build/deployment/debug scripts
├── .github/workflows/          # CI (lint, typecheck, test, build) + Deploy (SSH, PM2)
├── ecosystem.config.js         # PM2 process configuration
├── turbo.json                  # Turborepo task pipeline
├── biome.json                  # Linter/formatter config
└── pnpm-workspace.yaml         # Workspace root

Entry Points

Process Path Description
API Server apps/api/src/main.ts NestJS bootstrap (Helmet, CORS, rate limiting)
Root Module apps/api/src/app.module.ts Global guards, interceptors, filters
Worker apps/api/src/worker.ts BullMQ background job processor
Health apps/api/src/health.controller.ts Health check endpoint
Frontend apps/web/src/main.tsx React 19 + TanStack Router + Query + Faro + PostHog
Root Layout apps/web/src/routes/__root.tsx Theme, Toaster, PostHog tracking

Backend API

Modules & Services

Module Files Purpose
AuthModule module, service, controller, auth.ts Better Auth (email/password + Google OAuth)
UsersModule module, service, controller, spec Profile CRUD, password change, account deletion, OAuth connections
BrandsModule module, service, controller, spec Brand CRUD (cached, admin-managed)
PlansModule module, service, controller, spec Pricing plan CRUD (cached, admin-managed)
SubscriptionsModule module, service, controller, spec Create, activate, cancel, resume, extend subscriptions
PaymentsModule module, service, controller, spec Stripe card payments, EFT with receipt upload, admin approval
ReferralsModule module, service, controller, spec Referral code generation, application, tier-based rewards
VehiclesModule module, service, controller, spec VIN decode (multi-source fallback), vehicle history, brand access check
CategoriesModule module, service, controller, spec Hierarchical category tree, schema pictures
PartsModule module, service, controller, spec Parts by category, OEM code search
CatalogModule module, service, controller, dto VIN-less PL24 catalog browser: brands, models, category trees, parts
TranslationsModule module, service, controller, spec Automotive term translation (Redis → DB → Dictionary fallback)
AdminModule module, service, controller, spec Dashboard stats, user management, payment approval, analytics
AnalyticsModule module, service, controller Usage analytics tracking
EmailModule module, service Postal transactional emails (password reset, welcome, payment confirmation)
StorageModule module, service S3/MinIO file upload/download
RedisModule module, service, provider Key-value cache operations

Common Infrastructure

Type Name Behavior
Guard AuthGuard (global) Validates Better Auth session; skip with @Public()
Guard RolesGuard (global) Checks @Roles("admin") metadata against user.role
Guard BrandAccessGuard (per-route) Verifies user's subscription includes the target brand
Guard ThrottlerGuard (global) Rate limiting (100/min default)
Interceptor TransformInterceptor (global) Wraps responses: {success: true, data: ...}
Interceptor LoggingInterceptor (global) Logs method, URL, status, response time
Interceptor TimeoutInterceptor (global) 30s request timeout
Pipe VinValidationPipe (per-route) Validates VIN: 17 chars, alphanumeric, no I/O/Q
Filter HttpExceptionFilter (global) Returns {success: false, error: {code, message}}
Filter DrizzleExceptionFilter (global) Catches unique constraint violations → 409 Conflict
Middleware FileUploadValidation PNG/JPG/PDF only, max 5MB

Custom Decorators:

  • @Public() — Skip authentication
  • @CurrentUser(field?) — Inject authenticated user (or specific field)
  • @Roles(...roles) — Require role(s)
  • @ThrottleAuth() — 5 req/min
  • @ThrottleVinDecode() — 20 req/min
  • @ThrottleGeneral() — 100 req/min

Shared DTOs: ApiResponseDto, PaginationDto

Integrations

VIN Decode Fallback Chain: Corgi (offline WMI) → PartsCatalogs API → PL24 API → EMEX Scraper → NHTSA VIN API

Category Fetch Fallback Chain (per source): DB cache → PL24 → PartsCatalogs → EMEX (triggered by getCategoryTree)

Integration Type Path Notes
Corgi Offline DB corgi/ WMI database for brand identification (+ spec)
PL24 REST API pl24/ Multi-brand catalog API + auth + parsers (BMW, Mercedes, Generic, Ford Legacy)
PartsCatalogs REST API parts-catalogs/ Multi-brand catalog API. Files: service, auth-service, module, types. Supports fetchGroups, fetchParts with parameterized car queries
EMEX Browser scraper emex/ Playwright-based (emexdwc.ae), async via BullMQ. Files: service, browser, mapper, types
VIN-API REST API vin-api/ NHTSA VIN decoder (last-resort fallback)
Stripe Payment API Global payment processor for card payments
MinIO S3 API Receipt uploads, schema images

Database Schema

ORM: Drizzle ORM 0.41 with PostgreSQL

Schema files: apps/api/src/database/schema/

  • core.ts — Main application tables
  • emex.ts — EMEX scraper cache tables
  • pl24.ts — PL24 catalog cache tables
  • parts-catalogs.ts — PartsCatalogs API cache tables
  • relations.ts — Drizzle ORM relationships

Core Tables

users
  ├── id (uuid, PK)
  ├── name, email (unique), emailVerified, image
  ├── role (default: "user"), referralCode (unique), referredBy
  └── createdAt, updatedAt

sessions / accounts / verifications
  └── Better Auth managed tables

brands
  ├── id (uuid, PK), name, slug (unique), logoUrl, isActive
  └── createdAt, updatedAt

plans
  ├── id (uuid, PK), name, brandCount
  ├── priceMonthly, priceYearly, isActive
  └── createdAt, updatedAt

userSubscriptions
  ├── id (uuid, PK), userId → users, planId → plans
  ├── status (pending/active/cancelled/expired)
  ├── billingPeriod (monthly/yearly), startDate, endDate, cancelledAt
  └── Indexes: userId, status

userBrands (junction)
  ├── userId → users, subscriptionId → userSubscriptions, brandId → brands
  └── Unique: (userId, subscriptionId, brandId)

payments
  ├── id (uuid, PK), userId → users, subscriptionId → userSubscriptions
  ├── amount, currency, method (stripe/eft), status (pending/completed/failed/refunded)
  ├── stripePaymentIntentId, eftReceiptUrl, adminNote
  └── Indexes: userId, status

vehicles
  ├── id (uuid, PK), vin (unique), brandId → brands
  ├── brandName, model, year, engine, transmission, bodyType, market
  ├── rawData (jsonb), source
  └── Indexes: vin
  Note: Shared across users; access via userVehicles junction table

userVehicles (junction)
  ├── userId → users, vehicleId → vehicles
  ├── lastAccessedAt
  └── Unique: (userId, vehicleId)

categories
  ├── id (uuid, PK), vehicleId → vehicles (nullable), catalogVehicleId → catalogVehicles (nullable)
  ├── name, nameOriginal, parentId (self-ref), externalId, source
  └── Indexes: vehicleId, parentId
  Note: DUAL FK — exactly one of vehicleId (VIN-based) or catalogVehicleId (VIN-less) is non-null per row

parts
  ├── id (uuid, PK), vehicleId → vehicles (nullable), catalogVehicleId → catalogVehicles (nullable)
  ├── categoryId → categories, oemCode, name, nameOriginal, description, quantity, position, hotspotIndex
  └── Indexes: vehicleId, categoryId, oemCode
  Note: DUAL FK — same pattern as categories

schemaPics
  ├── id (uuid, PK), categoryId → categories
  ├── imageUrl, hotspots (jsonb), source
  └── Indexes: categoryId

queryLogs
  ├── id (uuid, PK), userId → users, vin, brandId → brands
  ├── source, success, errorMessage, responseTimeMs
  └── Indexes: (userId, createdAt), vin

referrals
  ├── id (uuid, PK), referrerId → users, referredId → users (unique)
  ├── rewardApplied, createdAt
  └── Indexes: referrerId, referredId

passwordResetTokens
  └── id, userId → users, token (unique), expiresAt, usedAt

emexCategoryTranslations
  └── id, originalName (unique), translatedName, isManual

catalogVehicles
  ├── id (uuid, PK), source (pl24), serviceName, brandName, brandId → brands
  ├── model, year, engine, bodyType, transmission, market
  ├── serviceVehicleId, catalogPath, architecture (P5_MODERN/P4_LEGACY)
  ├── metadata (jsonb), categoriesFetched (bool)
  └── Unique: (serviceName, serviceVehicleId)
  Note: categories and parts tables both have catalogVehicleId FK (nullable) for VIN-less catalog data

Integration Tables

  • pl24_* — PL24 catalog cache (catalogs, vehicles, VINs, part groups, parts, schemas)
  • emex_* — EMEX scraper cache (similar structure with translations)
  • parts_catalogs_* — PartsCatalogs API cache (defined in parts-catalogs.ts)

Migration Workflow

Strategy: Formal Drizzle SQL migrations with a custom hash-based runner.

The migration runner at apps/api/src/database/migrate.ts uses a custom hash-based runner instead of Drizzle's built-in migrate(). It tracks applied migrations by SHA256 of SQL file content rather than by timestamp comparison.

Why custom? Drizzle's built-in migrate() compares folderMillis timestamps from drizzle/meta/_journal.json. When drizzle-kit generate produces incorrect timestamps (clock skew bug), new migrations can be silently skipped. Hash-based tracking is immune to clock skew and provides deterministic idempotency.

How it works:

  1. Reads all previously-applied migration hashes from drizzle.__drizzle_migrations
  2. Reads drizzle/meta/_journal.json for the list of migration files in index order
  3. For each migration file, computes SHA256 of the SQL content
  4. If the hash already exists in __drizzle_migrations → skip
  5. If the hash is new → execute all statements in a single transaction, then record the hash

Idempotent baseline: 0000_brief_guardian.sql uses IF NOT EXISTS for all CREATE TABLE, CREATE INDEX, and CREATE UNIQUE INDEX statements. ALTER TABLE ADD CONSTRAINT statements are wrapped in DO $$ BEGIN ... EXCEPTION WHEN duplicate_object ... END $$ blocks. This allows the baseline to be safely re-applied on existing databases where __drizzle_migrations was bootstrapped to a different hash.

Schema change workflow:

  1. Edit schema files in apps/api/src/database/schema/
  2. Run pnpm db:generate — produces a timestamped .sql file in apps/api/drizzle/
  3. Review the SQL diff in PR (the migration is version-controlled alongside code)
  4. Commit and deploy — migration runs automatically on container startup

Container startup: The API server runs apps/api/start.sh which executes node dist/database/migrate.js BEFORE starting the NestJS server. This ensures all pending migrations are applied before any code runs.

Bootstrap note: Existing databases with no migration history (__drizzle_migrations table empty or missing) are handled automatically. The runner creates the tracking table and applies the baseline migration idempotently (all statements use IF NOT EXISTS guards, so no errors on re-application). This is safe for databases previously managed via pnpm db:push.

Manual commands:

  • pnpm db:migrate — Run migrations in dev (uses tsx for direct TS execution)
  • pnpm db:migrate:dist — Run migrations against compiled JS (for production manual override)
  • pnpm db:generate — Create a new migration from schema changes
  • pnpm db:push — Direct schema push (still available for local dev, discouraged for shared environments)

Worker process: The BullMQ worker overrides the Docker CMD in docker-compose.coolify.yml and does NOT run migrations. Only the API service handles migrations, preventing race conditions.

Job Queues

Framework: BullMQ with Redis

Queue Trigger Schedule Action
EMEX_SCRAPE On-demand (VIN decode) Scrapes EMEX via Playwright, stores results
CATALOG_PREFETCH On-demand (after VIN decode) Prefetches category tree + parts for all sources (depth-limited, rate-limited, cooldown-guarded). Handled by PrefetchWorkerService.
SUBSCRIPTION_EXPIRY Cron Daily 3:00 AM Expires ended subscriptions, removes brand access
QUERY_CLEANUP Cron Weekly Sun 4:00 AM Cleans old query log entries

Files: apps/api/src/jobs/jobs.module.ts, bull.config.ts, processors/, queues/, prefetch-worker.service.ts, prefetch-utils.ts, prefetch.types.ts

Telemetry

Location: apps/api/src/telemetry/

OpenTelemetry SDK with:

  • sdk-factory.ts — SDK initialization
  • tracing.ts — Distributed tracing (API)
  • worker-tracing.ts — Worker process tracing
  • metrics.ts — Prometheus metrics
  • __tests__/telemetry.spec.ts — Tests

Instrumentation: Express, HTTP, ioredis, NestJS Core, BullMQ, Drizzle ORM

API Endpoints

Global prefix: /api | Rate limit: 100 req/min (default)

Authentication

Method Path Auth Description
* /api/auth/** Public Better Auth catch-all (register, login, logout, OAuth, session)

Health

Method Path Auth Description
GET /api/health Public Health check ({status: "ok", timestamp})

Users

Method Path Auth Description
GET /api/users/me User Get current user profile
PATCH /api/users/me User Update profile (name, image)
POST /api/users/me/change-password User Change password
DELETE /api/users/me User Delete account
GET /api/users/me/connections User List OAuth connections
DELETE /api/users/me/connections/:provider User Unlink OAuth provider
GET /api/users Admin List all users (paginated)
GET /api/users/:id Admin Get user by ID

Brands

Method Path Auth Description
GET /api/brands Public List all brands (cached 30 min)
POST /api/brands Admin Create brand
PATCH /api/brands/:id Admin Update brand

Plans

Method Path Auth Description
GET /api/plans Public List all plans (cached 30 min)
POST /api/plans Admin Create plan
PATCH /api/plans/:id Admin Update plan

Subscriptions

Method Path Auth Description
POST /api/subscriptions User Create subscription with brand selection
GET /api/subscriptions/me User Get active subscription
PATCH /api/subscriptions/cancel User Cancel subscription
PATCH /api/subscriptions/resume User Resume cancelled subscription
GET /api/subscriptions Admin List all subscriptions (paginated)

Payments

Method Path Auth Description
POST /api/payments/stripe/checkout User Start Stripe card payment
POST /api/payments/stripe/webhook Public Stripe webhook callback
POST /api/payments/eft User Create EFT payment
POST /api/payments/eft/:id/receipt User Upload EFT receipt (PNG/JPG/PDF, 5MB max)
PATCH /api/payments/eft/:id/approve Admin Approve EFT payment
PATCH /api/payments/eft/:id/reject Admin Reject EFT payment
GET /api/payments/me User Payment history
GET /api/payments/pending Admin Pending EFT payments

Referrals

Method Path Auth Description
GET /api/referrals/stats User Referral count + reward tier status
GET /api/referrals/me User Referral code + list of referees
POST /api/referrals/apply User Apply a referral code

Vehicles

Method Path Auth Description
POST /api/vehicles/decode User Decode VIN (rate limited: 20/min)
GET /api/vehicles/history User Vehicle history (paginated)
GET /api/vehicles/:id User Get vehicle details
DELETE /api/vehicles/:id User Delete vehicle

Categories & Parts

Method Path Auth Description
GET /api/categories/tree/:vehicleId User Category tree for vehicle (cached)
GET /api/categories/:id User Category with schema pictures
GET /api/parts/category/:categoryId User Parts by category
GET /api/parts/search?oem= User Search parts by OEM code
GET /api/parts/:id User Part by ID

Catalog (VIN-less Browser)

Method Path Auth Description
GET /api/catalog/brands User List PL24 brands with access flags (hasAccess per subscription)
GET /api/catalog/brands/:brandName/catalogs User Service catalog list for a brand (multi-catalog brands like Ford)
GET /api/catalog/brands/:brandName/models User Model list for a brand (?service= optional) — DB cache → PL24 fetch
GET /api/catalog/vehicles/:id User Get a catalog vehicle by ID
GET /api/catalog/vehicles/:id/ford-config User Ford model-year/engine/gearbox options (Legacy Ford brands)
GET /api/catalog/vehicles/:id/psa-bodies User PSA body style options (Citroën/Peugeot)
GET /api/catalog/vehicles/:id/psa-engines User PSA engine options for a given body (?body=)
GET /api/catalog/vehicles/:id/psa-gearboxes User PSA gearbox options for a given body+engine (?body=&engine=)
GET /api/catalog/vehicles/:id/categories User Category tree (?body=&engine=&gearbox=); Redis cached 2h
GET /api/catalog/vehicles/:id/categories/:categoryId User Sub-categories or parts+schema (?body=&engine=&gearbox=); lazy PL24 fetch
POST /api/catalog/explore/:serviceName Admin Explore PL24 service structure (discovery tool)

Translations

Method Path Auth Description
GET /api/translations/:key Public Get translation
POST /api/translations/batch User Batch translate items
PUT /api/translations/:key Admin Set/override translation
GET /api/translations/search?q= Admin Search translations

Admin

Method Path Auth Description
GET /api/admin/dashboard Admin Dashboard stats (users, revenue, queries)
GET /api/admin/users Admin User list with search (paginated)
GET /api/admin/users/:id Admin User detail with subscriptions & payments
GET /api/admin/payments/pending Admin Pending EFT payments
GET /api/admin/query-logs Admin Query logs (paginated, filterable)
GET /api/admin/referrals Admin Referral stats (paginated)
GET /api/admin/stats/daily Admin Daily VIN decode stats (last 30 days)

Analytics

Method Path Auth Description
POST /api/analytics/oem-copy User Track OEM code copy event (oemCode, partId?, vehicleId?, categoryId?)

Changelog

Method Path Auth Description
GET /api/changelog Public List changelog entries (Redis-cached, 30min)
POST /api/changelog Admin Create changelog entry
POST /api/changelog/internal Token (Bearer) Create changelog entry (Fusion automation webhook, CHANGELOG_AUTOMATION_TOKEN env)
PATCH /api/changelog/:id Admin Update changelog entry
DELETE /api/changelog/:id Admin Delete changelog entry

Authentication

Provider: Better Auth 1.2

Flow:
  1. Client sends credentials to /api/auth/sign-in/email (or /sign-up, /oauth)
  2. Better Auth validates, creates session in DB
  3. Returns session cookie (better-auth.session_token, 5-min cache)
  4. Subsequent requests include cookie automatically
  5. AuthGuard calls auth.api.getSession() to validate
  6. User injected into request via @CurrentUser() decorator

User model extensions: role (default: "user"), referralCode, referredBy


Frontend Web

Routes & Pages

Router: TanStack Router (file-based, auto-generated route tree — 36 files)

Public

Path Route File Description
/ routes/index.tsx Landing page with VIN decode + features + pricing CTA
/pricing routes/pricing.tsx Plan comparison (1/2/3 brand, full package)
/about routes/about.tsx About page
/contact routes/contact.tsx Contact page
/blog routes/blog.tsx Blog listing page
/blog/:slug routes/blog_/$slug.tsx Blog post detail (SEO pre-rendered)
/demo routes/demo.tsx Demo page
/privacy routes/privacy.tsx Privacy policy
/terms routes/terms.tsx Terms of service
/kvkk routes/kvkk.tsx KVKK (Turkish data protection)

Auth (Layout: _auth.tsx)

Path Route File Description
/login routes/_auth/login.tsx Email/password + Google OAuth
/register routes/_auth/register.tsx Account creation
/forgot-password routes/_auth/forgot-password.tsx Email-based recovery
/reset-password routes/_auth/reset-password.tsx Token-based password reset

Dashboard (Protected, Layout: dashboard.tsx)

Path Route File Description
/dashboard routes/dashboard/index.tsx Dashboard home
/dashboard/search routes/dashboard/search.tsx VIN Search — main VIN decoder input
/dashboard/history routes/dashboard/history.tsx Past VIN decode searches
/dashboard/subscription routes/dashboard/subscription/index.tsx Plan selection & brand picker
/dashboard/subscription/pay routes/dashboard/subscription/pay.tsx Card (Stripe) or EFT payment
/dashboard/billing routes/dashboard/billing.tsx Payment history & receipts
/dashboard/settings routes/dashboard/settings.tsx Profile, Security, Connections, Referral, Account, Changelog tabs
/dashboard/vehicles/$id routes/dashboard/vehicles_/$id/index.tsx Vehicle details
/dashboard/vehicles/$id/categories/$categoryId routes/dashboard/vehicles_/$id/categories_/$categoryId.tsx Interactive schema + parts table

Catalog Browser (VIN-less, Protected)

Path Route File Description
/dashboard/catalog routes/dashboard/catalog/index.tsx Brand grid with hasAccess flags; locked brands show upgrade CTA
/dashboard/catalog/$brandName routes/dashboard/catalog_/$brandName/index.tsx Model list for brand (fetched from PL24)
/dashboard/catalog/$brandName/$modelId routes/dashboard/catalog_/$brandName_/$modelId/index.tsx Vehicle details + category tree (grid/tree toggle)
/dashboard/catalog/$brandName/$modelId/categories/$categoryId routes/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId.tsx Category sub-groups or schema+parts (lazy PL24 fetch)

Admin (Role-based)

Path Route File Description
/dashboard/admin routes/dashboard/admin/index.tsx Stats overview with charts
/dashboard/admin/users routes/dashboard/admin/users.tsx User management
/dashboard/admin/payments routes/dashboard/admin/payments.tsx EFT approval workflow
/dashboard/admin/referrals routes/dashboard/admin/referrals.tsx Referral program tracking
/dashboard/admin/analytics routes/dashboard/admin/analytics.tsx Daily query statistics
/dashboard/admin/copy-logs routes/dashboard/admin/copy-logs.tsx OEM code copy tracking

Components

Component Location Description
SchemaViewer components/schema/schema-viewer.tsx Interactive parts diagram with zoom/pan/fullscreen
HotspotOverlay components/schema/hotspot-overlay.tsx SVG overlay for clickable part regions
PartsPanel components/schema/parts-panel.tsx Parts table with OEM codes, synced with schema
SchemaToolbar components/schema/schema-toolbar.tsx Zoom/reset/fullscreen controls
BrandSelector components/subscription/brand-selector.tsx Brand grid with plan-enforced max selection
VehicleCard components/vehicles/vehicle-card.tsx Vehicle info card (VIN, brand, model, year)
VehicleSelectModal components/vehicles/vehicle-select-modal.tsx Modal for selecting a vehicle from history (used during VIN decode flow)
VinInput components/vehicles/vin-input.tsx VIN entry input component
CategoryTree components/categories/category-tree.tsx Recursive expandable category hierarchy
CategoryGrid components/categories/category-grid.tsx Grid layout for category browsing
DailyChart components/admin/daily-chart.tsx Daily VIN decode stats chart
PaymentContent components/payment/payment-content.tsx Payment form and flow
SettingsContent components/settings/settings-content.tsx User settings panel content
ChangelogTab components/settings/changelog-tab.tsx Timeline + accordion changelog viewer
PsaVariantSelector components/catalog/psa-variant-selector.tsx PSA (Citroën/Peugeot) body/engine/gearbox picker for VIN-less catalog
FordVariantSelector components/catalog/ford-variant-selector.tsx Ford model-year/engine/gearbox picker for VIN-less catalog

UI primitives from @sase/ui: Button, Card, Input, Label, Badge (with stage variant), Dialog, Tabs, Separator, Skeleton, Accordion

Hooks & Stores

Hooks:

Hook Purpose
useAuth() Auth state + Better Auth client (user, isAdmin, signIn, signUp, signOut, session)
useCategoryParts(vehicleId, categoryId) TanStack Query for schema + parts + hotspots
useSchemaInteraction() Pan/zoom/pinch event handlers for schema viewer
usePageMeta(options) Sets <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
useSchemaStore() { highlightedGroup, selectedGroup, zoom, panX, panY, isFullscreen }

Data Fetching: Custom ApiClient class (lib/api-client.ts)

  • Base URL: /api (proxied via Vite dev server)
  • Methods: get<T>, post<T>, patch<T>, delete<T>, upload<T>
  • Credentials: cookies (automatic)
  • Caching: TanStack React Query 5 (staleTime: 60s, retry: 1, no refetch on focus)

Lib Utilities

File Purpose
api-client.ts HTTP client wrapper with Faro error integration
auth-client.ts Better Auth client (signIn, signUp, signOut, useSession)
i18n.ts Zustand-based i18n (tr/en), useTranslation() hook
posthog.ts PostHog analytics initialization and tracking
faro.ts Grafana Faro frontend observability
user-settings.ts Theme/settings persistence
toast.ts Toast notification export
category-icons.ts Category icon mappings

Internationalization

Implementation: Custom Zustand store with JSON message files

Locale File Coverage
Turkish (default) messages/tr.json Full
English messages/en.json Full

Usage: const { t, locale, setLocale } = useTranslation()t("nav.search")

Persistence: localStorage("sase-locale"), updates document.documentElement.lang


Shared Packages

@sase/shared (packages/shared/src/)

Directory Exports
types/ User, UserProfile, UserSubscriptionSummary, Vehicle, VinDecodeResult, CategoryNode, VehicleSource, Brand, Plan, Subscription, UserBrand, CreateSubscriptionInput, SubscriptionStatus, Payment, PaymentMethod, PaymentStatus, EftPaymentInput, Part, PartSource, PartSearchResult, Category, CategoryWithSchema, SchemaPic, Hotspot, ApiResponse, ApiError, PaginationMeta, PaginationInput, PaginatedResult, ChangelogEntry, CreateChangelogEntry, UpdateChangelogEntry, ChangelogChangeType
schemas/ loginSchema, registerSchema, forgotPasswordSchema, resetPasswordSchema, vinSchema, paginationSchema, changelogEntrySchema, createChangelogEntrySchema, updateChangelogEntrySchema, changelogChangeTypeEnum (Zod)
constants/ ERROR_CODES (30+, prefixed AUTH/VIN/SUB/PAY), PLANS (Single/Double/Triple/Full), REFERRAL_REWARDS (Tier 1: 3→7d, Tier 2: 5→30d), VIN_REGEX, EMAIL_REGEX, OEM_CODE_REGEX, CURRENCY
utils/ VIN validator (check digit, WMI extraction, model year decode), currency (formatTRY, kurus↔lira), formatters (VIN, date, datetime, Turkish slug, referral code)

Dependency: zod ^3.24.0

@sase/config (packages/config/src/index.ts)

Zod env schema exporting envSchema, Env type, validateEnv(). Groups: DATABASE_URL, REDIS_, BETTER_AUTH_, GOOGLE_, MINIO_, CORS_ORIGIN, STRIPE_, PL24_, EMEX_, ML_PREDICTION_ENABLED, POSTAL_, OTEL_*

@sase/ui (packages/ui/src/)

Components: Button (CVA variants), Input, Card (6 compound parts), Badge (CVA, includes stage variant: alpha/beta/prod), Label, Skeleton, Separator, Dialog (10 compound parts), Tabs (4 compound parts), Accordion (4 compound parts), cn() utility

Dependencies: Radix UI (accordion, dialog, dropdown-menu, label, popover, select, separator, slot, tabs, tooltip), class-variance-authority, clsx, tailwind-merge, lucide-react


Testing

Unit Tests (Vitest 3)

Category Files Total Lines
Service specs 12 (admin, brands, categories, changelog, parts, payments, plans, referrals, subscriptions, translations, users, vehicles) 2,521
Guard specs 3 (auth, roles, brand-access) 380
Pipe specs 1 (vin-validation) 77
Integration specs 1 (corgi) 121
Telemetry specs 1 97
Total API 18 test files 3,196 lines

Test Config:

  • API: apps/api/vitest.config.tssrc/**/*.spec.ts, v8 coverage (text + lcov)
  • Web: apps/web/vitest.config.tssrc/**/*.{test,spec}.{ts,tsx}, jsdom, @/ alias, test-setup (jest-dom)

Test Pattern: Mock Drizzle DB with chainable query builder, vi.mock() for external deps

Playwright 1.50

  • Installed at root level (package.json)
  • Used by EMEX integration (emex.browser.ts) for web scraping
  • Used by PartsCatalogs auth service for JWT capture via Playwright
  • Test scripts in scripts/ (vin-e2e-test.js)

Infrastructure & Deployment

Docker Compose (Development)

Service Image Port Volume
PostgreSQL 17 postgres:17-alpine 127.0.0.1:5432 pg_data
Redis 7.4 redis:7.4-alpine 127.0.0.1:6379 redis_data
MinIO minio/minio 9000 (API), 9001 (Console) minio_data

SEO Infrastructure

  • apps/web/scripts/prerender.mjs — Pre-renders public pages to static HTML (landing, blog posts, pricing, etc.) for crawler/bot visibility
  • apps/web/public/robots.txt — Crawl directives with sitemap reference
  • apps/web/public/sitemap.xml — Static sitemap for public pages
  • apps/web/index.html — Contains default OG/Twitter meta tags; usePageMeta() overrides at runtime per page

Nginx (docker/nginx/sites/)

  • sase.tr.conf — Frontend SPA + /api proxy + /collect/ Faro telemetry CORS proxy + gzip (level 6) + 1-year asset cache + security headers
  • api.sase.tr.conf — NestJS proxy (60s timeout for VIN decode) + SSL + blocked paths (.git, .env, node_modules)

CI/CD (GitHub Actions)

ci.yml — Runs on all branches & PRs to main (15min timeout):

  1. Biome lint
  2. TypeScript type check
  3. Vitest unit tests
  4. Full build

deploy.yml — Runs on push to main (10min timeout):

  1. SSH into production
  2. git pull origin main
  3. pnpm install
  4. pnpm build
  5. pnpm db:migrate (migrations)
  6. PM2 reload all

PM2 Configuration

Process Command Port Memory
sase-api pnpm dev (dev) / dist/main.js (prod) 4000 512MB
sase-web pnpm dev (dev) / serve dist (prod) 3000 512MB
sase-worker dist/worker.js 256MB

Environment Variables

Required

Variable Description
DATABASE_URL PostgreSQL connection string
REDIS_PASSWORD Redis authentication
BETTER_AUTH_SECRET Auth encryption key (min 32 chars)
BETTER_AUTH_URL Auth service URL
MINIO_ENDPOINT MinIO/S3 endpoint
MINIO_ACCESS_KEY MinIO access key
MINIO_SECRET_KEY MinIO secret key
MINIO_PUBLIC_URL Public URL for stored files
CORS_ORIGIN Allowed origins

Optional

Variable Default Description
PORT 4000 API server port
REDIS_HOST 127.0.0.1 Redis host
REDIS_PORT 6379 Redis port
MINIO_BUCKET_NAME sase-schemas Storage bucket
MINIO_USE_SSL false HTTPS for MinIO
GOOGLE_CLIENT_ID Google OAuth client ID
GOOGLE_CLIENT_SECRET Google OAuth secret
STRIPE_SECRET_KEY Stripe payment secret key
STRIPE_PUBLISHABLE_KEY Stripe publishable key
PL24_BASE_URL PL24 catalog API URL
PL24_COMPANY_CODE PL24 company code
PL24_USERNAME PL24 credentials
PL24_PASSWORD PL24 credentials
EMEX_USERNAME EMEX scraper credentials
EMEX_PASSWORD EMEX scraper credentials
ML_PREDICTION_ENABLED false Enable ML predictions
POSTAL_API_URL Postal email service URL
POSTAL_API_KEY Postal API key
POSTAL_FROM_ADDRESS noreply@sase.tr Sender email
POSTAL_FROM_NAME Sase.tr Sender name
OTEL_ENABLED false Enable OpenTelemetry
OTEL_EXPORTER_OTLP_ENDPOINT OTLP collector endpoint
OTEL_EXPORTER_OTLP_HEADERS OTLP auth headers
OTEL_SERVICE_NAME sase-api OTel service name
OTEL_TRACE_SAMPLE_RATE 1.0 Trace sampling rate (0-1)

Full schema: packages/config/src/index.ts


Key Commands

# Development
pnpm dev                        # Start all apps (Turbo)
pnpm dev --filter=api           # API only
pnpm dev --filter=web           # Web only

# Build & Quality
pnpm build                      # Build all packages + apps
pnpm lint                       # Biome lint check
pnpm typecheck                  # TypeScript --noEmit
pnpm test                       # Run all tests (Vitest)

# Database (run from apps/api/)
pnpm db:push                    # Push schema to DB (Drizzle)
pnpm db:studio                  # Open Drizzle Studio
pnpm db:seed                    # Seed database
pnpm db:generate                # Generate migration

# Route generation
pnpm --filter web exec tsr generate

# Production
pm2 start ecosystem.config.js
pm2 reload all

Key Dependencies

Package Version Purpose
NestJS 10.4 Backend framework
Drizzle ORM 0.41 Database ORM
Better Auth 1.2 Cookie-based auth
React 19 Frontend UI
TanStack Router 1.120 File-based routing
TanStack Query 5 Server state management
Zustand 5 Client state management
Tailwind CSS 4 Styling
Vite 6.3 Build tool
Vitest 3.x Unit testing
Playwright 1.50 Browser automation
BullMQ 5.30 Job queues
Biome latest Linting/formatting
Turborepo 2.x Monorepo orchestration
OpenTelemetry 0.212 Backend observability
Grafana Faro 2.2 Frontend observability
PostHog latest Product analytics

Quick Start

  1. docker compose -f docker/docker-compose.yml up -d — Start PostgreSQL, Redis, MinIO
  2. cp apps/api/.env.example apps/api/.env — Configure env vars
  3. pnpm install — Install dependencies
  4. pnpm --filter api db:push && pnpm --filter api db:seed — Setup database
  5. pnpm dev — Start all services
  6. Open http://localhost:3000 — Frontend
  7. Admin login: admin@sase.tr / Sase2026

Pricing Model

Plan Brands Monthly Yearly
1 Marka 1 200 TRY 2,000 TRY
2 Marka 2 350 TRY 3,500 TRY
3 Marka 3 500 TRY 5,000 TRY
Full Paket Unlimited 999 TRY 9,990 TRY

Referral Rewards:

  • Tier 1: 3 referrals → 7-day subscription extension
  • Tier 2: 5 referrals → 30-day subscription extension

Documentation

File Topic
docs/INDEX.md This file — comprehensive project reference
CLAUDE.md Claude Code project guide
.claude/product-marketing-context.md Marketing context
docs/00-overview.mddocs/13-analytics-posthog.md Detailed topic docs