# Meshkee CMS API — Project Context > Living reference for developers and AI assistants working on this codebase. > Last updated: July 24, 2026 ## What This Project Is **Meshkee CMS API** (`meshkee-cms-api`) is a multi-tenant backend for Meshkee business websites. Each business gets its own domain, content (products, categories, media), team members, and customer registrations. Platform super admins manage businesses and domains; business owners and staff manage per-business content through a permission-based dashboard. **API base URL:** `/api/v1` --- ## Tech Stack | Layer | Technology | |-------|------------| | Runtime | Node.js, TypeScript (ES2021, strict) | | Framework | NestJS 11 | | ORM | Prisma 6 (`prisma db pull` — schema is introspected, not migrated via Prisma) | | Database | PostgreSQL 16 | | Cache | Redis 7 (OTP storage) | | Auth | JWT (access + refresh), Passport, bcrypt | | Validation | class-validator + class-transformer | | File storage | S3-compatible (Parmin), Sharp for image processing | --- ## Architecture ### Multi-tenancy model ``` Platform (super admin) └── Business (tenant root) ├── Domains (host → business resolution) ├── Team members (BusinessUser + Role) ├── Customers (BusinessCustomer) ├── Categories (per entityType: product | blog | portfolio) ├── Products │ └── Store items (product variants — price & stock per variation combo) ├── Media library └── Settings (JSON) ``` - **Tenant resolution:** `GET /tenants/:host` resolves a domain to a business (public, no auth). - **Business-scoped APIs:** Most CMS routes use `businesses/:businessId/...` with JWT + business permission checks. - **Platform APIs:** User/business/domain management requires `super_admin` (checked in services). ### Two "category" concepts | Concept | Table | Purpose | |---------|-------|---------| | **Content Category** | `categories` | Per-business taxonomy for products, blogs, portfolios | | **Business Category** | `business_categories` | Platform-wide taxonomy classifying businesses (Retail, Creative, etc.) | Do not confuse them when reading or writing code. --- ## Directory Structure ``` src/ ├── main.ts # Bootstrap, global prefix, pipes, interceptors ├── app.module.ts # Root module wiring ├── auth/ # JWT, OTP/SMS, permissions, guards, decorators ├── users/ # Super-admin user management ├── roles/ # Global + team role listing ├── business-admin/ # Super-admin business CRUD + domains ├── business-team/ # Per-business team invite/manage ├── tenant/ # Domain → business resolution (public) ├── domain-admin/ # Super-admin domain management ├── categories/ # Content categories, variations, technical forms ├── products/ # Products, variants, technical info ├── cart/ # Customer shopping cart ├── orders/ # Customer checkout + admin order management ├── media/ # Upload/list/update/delete media ├── storage/ # S3 driver abstraction ├── website-docs/ # Public website API docs pack ├── invoices/ # Platform invoices + item templates (super-admin; business-ready schema) ├── prisma/ # PrismaModule + PrismaService ├── redis/ # Redis client + OTP helpers └── common/ # Shared interceptors (BigInt serialization) prisma/schema.prisma # Introspected from PostgreSQL (source of truth after migrations) database/ ├── migrations/ # Raw SQL migrations (numbered) ├── seeds/ # Dev sample data + production super-admin ├── migrate.sh, seed.sh, setup.sh postman/ # API collection for manual testing ``` ### NestJS module pattern Each feature follows: `*.module.ts` → `*.controller.ts` → `*.service.ts` → `dto/` --- ## Getting Started ```bash cp .env.example .env docker compose up -d # Postgres (auto-runs migrations on first init) + Redis ./database/seed.sh # Sample dev data npm install npm run prisma:generate npm run start:dev # http://localhost:3000/api/v1 ``` **Dev credentials** (after `001_sample_data.sql` seed): any seeded user, password `password`. Example super admin: `+989121111111` / `password` --- ## Database & Migrations ### Workflow 1. Write a new SQL file in `database/migrations/` (e.g. `012_feature.sql`). 2. Apply via `./database/migrate.sh` or `docker exec` into Postgres. 3. Run `npm run prisma:pull` to sync `prisma/schema.prisma`. 4. Run `npm run prisma:generate` to regenerate the client. **Important:** This project does **not** use Prisma Migrate. SQL migrations are the authoritative schema history. Prisma schema is maintained separately via introspection. ### Migration files | File | Purpose | |------|---------| | `001_initial_schema.sql` | Users, businesses, domains | | `002_phone_permissions_content_media.sql` | RBAC, media, products, blogs, portfolios | | `003_categories.sql` | Content categories + assignments | | `004_user_types_and_business_members.sql` | Super admin, business owner, customer roles | | `005_business_team_roles.sql` | Team roles (admin, editor, viewer) | | `006_business_categories.sql` | Platform business category taxonomy | | `006_user_profile.sql` | User profile JSON | | `007_business_i18n_fields.sql` | Business Persian name | | `007_domain_expiry_active.sql` | Domain expiry/active flags | | `008_remove_name_en.sql` | Schema cleanup | | `009_categories_name_fa.sql` | Category `name_fa` | | `010_category_variations.sql` | Category variations, product variants (store items) | | `011_category_technical_forms.sql` | Technical forms + product values | | `012_comments.sql` | Product comments | | `013_expert_reviews.sql` | Expert reviews | | `014_addresses.sql` | User/business street addresses | | `015_business_profile.sql` | Business profile fields | | `015_cities.sql` | Location cities (country → province → city) | | `016_product_variation_values.sql` | Product-level variation option selections (input for store items) | | `017_product_variant_festival.sql` | Festival flag on store items | | `018_product_variant_reward_points.sql` | Reward points on store items | | `019_cart_and_orders.sql` | Shopping cart, orders, order items + order permissions | | `020_store_items_and_variants.sql` | `store_items` + `store_item_variants` (replaces `product_variants`) | | `036_invoices.sql` | Invoices, invoice items, invoice item templates + permissions | | `037_invoice_name.sql` | Optional `invoices.name` | | `038_invoice_templates.sql` | Full invoice templates + key points/accounts on invoices | | `039_invoice_account_holder.sql` | `account_holder_name` on invoice / template accounts | Docker mounts `./database/migrations` into Postgres init — migrations run automatically only on **first** volume creation. Use `migrate.sh` for subsequent migrations. --- ## Domain Model ### Enums | Enum | Values | |------|--------| | `MediaEntityType` | `product`, `blog`, `portfolio` | | `ContentStatus` | `draft`, `published`, `archived` | | `VariationType` | `color`, `size`, `custom` | | `OrderStatus` | `pending`, `confirmed`, `processing`, `shipped`, `delivered`, `cancelled` | | `OrderSource` | `website`, `admin` | | `InvoiceOwnerScope` | `platform`, `business` | | `InvoiceStatus` | `draft`, `issued`, `paid`, `cancelled` | | `TechnicalFieldType` | `text`, `textarea`, `select`, `multi_select` | | `MediaType` | `image`, `video` | ### Core relationships ``` Business 1──* Domain Business 1──* Category (entityType: product|blog|portfolio) Business 1──* Product Business 1──* Media Category 1──* CategoryVariation 1──* CategoryVariationOption Category 1──0..1 CategoryTechnicalForm 1──* CategoryTechnicalFormField CategoryTechnicalFormField 1──* CategoryTechnicalFormFieldOption Product *──0..1 Category (via CategoryAssignment) Product 1──* ProductVariationValue → CategoryVariationOption (which options this product offers) Product 1──* ProductVariationValue → CategoryVariationOption (which options this product offers) Product 1──0..1 StoreItem (one shop listing per product) StoreItem 1──* StoreItemVariant (purchasable SKUs: price, stock, variation combo) StoreItemVariant 1──* StoreItemVariantSelection → CategoryVariationOption Product 1──* ProductTechnicalFieldValue → CategoryTechnicalFormField Business 1──* Cart (per customer) 1──* CartItem → ProductVariant Business 1──* Order 1──* OrderItem → ProductVariant (snapshot on order) User 1──* Cart, Order (as customer) Business 1──* Invoice (billed party) 1──* InvoiceItem InvoiceItemTemplate (platform or per-business predefined lines) Invoice.issuedBy → User Media 1──* MediaAttachment (polymorphic: entityType + entityId) ``` ### Schema gap: blogs & portfolios SQL migrations create `blogs` and `portfolios` tables and seed data populates them. Permissions exist (`blogs.*`, `portfolios.*`). However: - No Prisma models for Blog/Portfolio - No NestJS modules or API endpoints Categories and media attachments already support `blog` and `portfolio` entity types — infrastructure is ready, API is not. --- ## API Reference All routes are prefixed with `/api/v1`. ### Public | Method | Path | Description | |--------|------|-------------| | POST | `/auth/register` | Customer registration by domain | | POST | `/auth/login` | Cell + password | | POST | `/auth/refresh` | Refresh token | | POST | `/auth/send-otp` | Send OTP (Redis-backed) | | POST | `/auth/verify-otp` | Verify OTP | | GET | `/tenants/:host` | Resolve business from domain | | GET | `/tenants/:host/store-specials` | Active store specials | | GET | `/tenants/:host/website/category-groups` | Homepage category rows | | GET | `/tenants/:host/website/brand-groups` | Homepage brand rows | | GET | `/tenants/:host/website/sliders` | Homepage sliders with slides | ### Authenticated (JWT) | Method | Path | Access | |--------|------|--------| | GET | `/auth/me` | Any user | | PATCH | `/auth/profile` | Any user | | POST | `/auth/change-password` | Any user | | GET | `/roles?scope=global\|team` | Super admin / team.read | | GET | `/business-categories` | Super admin or `business_categories.read` | ### Super admin only (service-level check) | Module | Base path | |--------|-----------| | Users | `/users` | | Businesses | `/businesses` | | Domains | `/domains` | ### Business-scoped (JWT + `BusinessPermissionGuard`) Pattern: `/businesses/:businessId/` | Module | Base path | Key permissions | |--------|-----------|-----------------| | Team | `/team` | `business.team.*` | | Media | `/media` | `media.*` | | Categories | `/categories` | `categories.*` | | Products | `/products` | `products.*` | | Website | `/website/category-groups`, `/website/brand-groups`, `/website/sliders` | `website.*` | #### Categories — notable sub-routes | Method | Path | Description | |--------|------|-------------| | GET | `/categories/color-presets` | Predefined color palette | | GET/PUT | `/categories/:id/variations` | Category variation options | | GET/PUT | `/categories/:id/technical-form` | Technical form definition | #### Products — notable sub-routes | Method | Path | Description | |--------|------|-------------| | GET/PUT | `/products/:id/variations` | Which category variation options apply to this product | | GET/POST/PATCH/DELETE | `/products/:id/variants` | Removed — use `/store-items` | | GET/PUT | `/products/:id/technical-info` | Product technical data | #### Cart (customer — JWT, must be business customer) | Method | Path | Description | |--------|------|-------------| | GET | `/cart` | Get current user's cart | | POST | `/cart/items` | Add store item variant to cart (`storeItemVariantId`, `quantity`) | | PATCH | `/cart/items/:itemId` | Update cart item quantity | | DELETE | `/cart/items/:itemId` | Remove cart item | | DELETE | `/cart` | Clear cart | | POST | `/cart/checkout` | Place order from cart (requires `addressId` or `shippingAddress`) | #### Orders | Method | Path | Access | Description | |--------|------|--------|-------------| | GET | `/orders` | Customer (own) or `orders.read` (all) | List orders | | GET | `/orders/:orderId` | Customer (own) or `orders.read` | Order detail | | POST | `/orders` | `orders.create` | Admin: create order for a customer | | PATCH | `/orders/:orderId` | `orders.update` | Admin: update status / admin notes | --- ## Auth & RBAC ### Global user roles | Role slug | Dashboard | Notes | |-----------|-----------|-------| | `super_admin` | `super_admin` | Full platform access | | `business_owner` | `business` | Assigned to business owners | | `business_staff` | `business` | Legacy; team uses per-business roles | | `customer` | `customer` | Website registrants | ### Per-business team roles Assigned via `business_users.role_id`: | Role | Slug | Typical access | |------|------|----------------| | Owner | `isOwner=true` | All `business_owner` permissions | | Admin | `admin` | Full content + team read | | Editor | `editor` | Create/edit/publish content | | Viewer | `viewer` | Read-only | ### Permission groups (seeded) `business.*`, `domains.*`, `products.*`, `blogs.*`, `portfolios.*`, `media.*`, `categories.*`, `users.*`, `roles.manage`, `business.team.*`, `business_categories.*` Each resource typically has: `read`, `create`, `update`, `delete` (+ `publish` for content). ### Guards & decorators ```typescript @UseGuards(JwtAuthGuard, BusinessPermissionGuard) @RequireBusinessPermission('products.read') ``` - `JwtAuthGuard` — validates Bearer JWT (`type: 'access'`) - `BusinessPermissionGuard` — checks permission for `businessId` route param - `PermissionsService` — `isSuperAdmin()`, `hasBusinessPermission()` - Super admins bypass business permission checks - `@CurrentUser()` injects `AuthUser` into handlers ### Auth flow notes - Registration resolves tenant by `domain` → creates/links user → assigns `customer` role - OTP stored in Redis (`otp:{cellNumber}`), 5-min TTL; disabled when `SMS_ENABLED=false` - JWT payload: `sub`, `cellNumber`, `roles`, `dashboard`, `type` --- ## Key Features ### Products - CRUD with slug, status, featured media, gallery attachments - One category per product (via `CategoryAssignment`) - Content JSON: `{ nameFa, html }`; metadata JSON: `{ tags }` - i18n: Persian name in `content.nameFa`, summary in `description` ### Category variations (product categories) - Types: `color` (preset palette), `size`, `custom` - One color + one size per category (DB partial unique indexes) - `PUT /categories/:id/variations` replaces all variations (delete-all + recreate) - Color presets: `GET /categories/color-presets` (15 named colors with hex) ### Store (shop listings) The **store** turns CMS products into purchasable items using a two-level model: ``` Product variations → which options this product offers (product_variation_values) Store item → one shop listing per product (store_items) Store item variants → purchasable SKUs with price/stock per combination (store_item_variants) ``` | Layer | Table | API | Purpose | |-------|-------|-----|---------| | Product variations | `product_variation_values` | `PUT /products/:id/variations` | Which category variation options apply to this product | | Store item | `store_items` | `GET /store-items/by-product/:productId` | One listing per product in the shop | | Store item variants | `store_item_variants` + `store_item_variant_selections` | `POST /store-items`, `PUT /store-items/sync` | Purchasable combinations with price & stock | **Store item variant fields** (`store_item_variants`): | Field | DB column | Notes | |-------|-----------|-------| | `sku` | `sku` | Optional merchant SKU | | `price` | `price` | Selling price (`NUMERIC(12,2)`) | | `compareAtPrice` | `compare_at_price` | Optional strike-through / was-price | | `stockQuantity` | `stock_quantity` | Integer ≥ 0; `null` = untracked | | `isActive` | `is_active` | Whether variant is available for purchase | | `selections` | `store_item_variant_selections` | Exactly one `CategoryVariationOption` per category variation | **Rules:** - One `store_items` row per product per business. - Create variants under the store item after setting product variation values. - Each variant picks one option per variation; the combination must be unique per store item. - Cart and orders reference `storeItemVariantId` (not the product directly). **Example — batch create variants for a product:** ```json POST /businesses/:businessId/store-items { "productId": "1", "items": [ { "selections": [ { "variationId": "1", "optionId": "1" }, { "variationId": "2", "optionId": "5" } ], "price": 99.99, "stockQuantity": 10 } ] } ``` **Example — update a variant:** ```json PATCH /businesses/:businessId/store-items/variants/:variantId { "price": 89.99, "stockQuantity": 25 } ``` ### Technical forms (product categories) - One form per product category - Field types: `text`, `textarea`, `select`, `multi_select` - `PUT /categories/:id/technical-form` — define/replace form fields - `PUT /products/:id/technical-info` — fill product values (validated against category form) - Product must have a category assignment **Example — define form:** ```json PUT /businesses/:businessId/categories/:categoryId/technical-form { "fields": [ { "label": "Weight", "type": "text", "isRequired": true }, { "label": "Material", "type": "select", "options": ["Cotton", "Polyester"] }, { "label": "Features", "type": "multi_select", "options": ["Waterproof", "Breathable"] } ] } ``` **Example — fill product data:** ```json PUT /businesses/:businessId/products/:productId/technical-info { "values": [ { "fieldKey": "weight", "value": "500g" }, { "fieldKey": "material", "value": "cotton" }, { "fieldKey": "features", "value": ["waterproof", "breathable"] } ] } ``` Field keys are auto-slugified from labels (e.g. `"Weight"` → `"weight"`). ### Media - Multipart upload to S3 via Sharp processing - Polymorphic attachments to products (and future blog/portfolio entities) - Featured image on products via `featuredMediaId` --- ## Coding Conventions ### DTOs - `class-validator` decorators on all request bodies and query params - `@Type(() => Number)` for query param coercion - Slug format: `^[a-z0-9]+(?:-[a-z0-9]+)*$` - Cell numbers: E.164 `^\+[1-9]\d{6,14}$` ### Services - Inject `PrismaService`, `PermissionsService` - Convert route param IDs with `BigInt(idRaw)` - Private `assertPermission()` / `assertSuperAdmin()` helpers - Use `$transaction` for multi-step writes - **Replace semantics** for nested resources (variations, technical forms) — delete-all then recreate ### Error handling Use Nest exceptions: `NotFoundException`, `ForbiddenException`, `BadRequestException`, `ConflictException`, `UnauthorizedException`. ### Serialization - Global `BigIntSerializerInterceptor` converts BigInt → Number in JSON responses - Services use private `serialize()` methods for consistent output shapes - IDs returned as strings in API responses ### Global validation pipe (`main.ts`) ```typescript new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }) ``` --- ## Environment Variables See `.env.example` for the full list. Key groups: | Group | Variables | |-------|-----------| | Database | `DATABASE_URL`, `POSTGRES_*` | | Redis | `REDIS_URL`, `REDIS_HOST`, `REDIS_PORT` | | API | `PORT` | | JWT | `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`, `JWT_*_EXPIRES_IN` | | SMS | `SMS_ENABLED` | | S3 | `S3_ENDPOINT`, `S3_BUCKET`, `S3_PUBLIC_URL`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` | | Media | `MEDIA_MAX_FILE_SIZE_MB` | --- ## Implemented vs Planned ### Implemented - Multi-tenant auth (register, login, OTP, profile) - Super admin: users, businesses, domains, system business categories - Business team management - Media upload (S3 + Sharp) - Categories (all entity types in DB; API supports `entityType` filter) - Products CRUD - Product variation values (which options a product offers) - Store items / product variants (price, stock, SKU) - Shopping cart + checkout + orders (customer + admin) - Product technical info - Category variations & technical forms - Tenant resolution by domain - RBAC with granular permissions ### Planned / partial | Feature | DB | Permissions | API | Prisma | |---------|----|-------------|-----|--------| | Blogs | Yes | Yes | No | No model | | Portfolios | Yes | Yes | No | No model | | Customer dashboard | Partial | No | Register only | Yes | | SMS provider | — | — | Stub | — | | Store checkout (cart, orders) | Yes | Yes | Yes | Yes | | Customer favorites | — | `favorites.*` seeded | No | No | --- ## Invoices (platform / super-admin) Super admins issue invoices **to** a business. Schema is ready for future business-scoped issuing (`owner_scope=business`). ### Tables | Table | Purpose | |-------|---------| | `invoice_item_templates` | Predefined line items (`owner_scope` platform \| business) | | `invoice_templates` | Full blueprints: name, top_text | | `invoice_template_items` / `_key_points` / `_accounts` | Nested template content | | `invoices` | Invoice header (`business_id` = billed party, optional `name`, `top_text`, `notes`, `invoice_template_id`, `status`) | | `invoice_items` / `invoice_key_points` / `invoice_accounts` | Issued invoice nested content | ### API (super_admin only today) | Method | Path | |--------|------| | GET/POST | `/invoice-item-templates` | | PATCH/DELETE | `/invoice-item-templates/:templateId` | | GET/POST | `/invoice-templates` | | GET/PATCH/DELETE | `/invoice-templates/:templateId` | | GET/POST | `/businesses/:businessId/invoices` | | GET/PATCH/DELETE | `/businesses/:businessId/invoices/:invoiceId` | | GET | `/public/invoices/:invoiceId` (no auth; issued/paid only) | Auth (admin routes): `JwtAuthGuard` + service `assertSuperAdmin`. Serialized platform invoices include `publicUrl`: `https://{INVOICE_PUBLIC_DOMAIN}/invoices/{id}` (default `meshkee.com`), or `{INVOICE_PUBLIC_BASE_URL}/invoices/{id}` when set. Accounts include optional `accountHolderName`. Public HTML viewer lives in the dashboards super-admin SPA (`/invoices/:id`); API serves JSON via `/public/invoices/:id`. Permissions seeded for future business dashboard: `invoices.*`, `invoice_templates.*`. Module: `src/invoices/` · Migrations: `036`, `037`, `038`, `039` --- ## Testing **Postman collection:** `postman/Meshkee-CMS-Auth.postman_collection.json` Variables: `baseUrl`, `accessToken`, `refreshToken`, `domain`, `businessId`, `categoryId`, `productId`, `variantId`, `variationId`, `optionId`, `cartItemId`, `orderId` **Store workflow in Postman:** `Business Categories` → set variations → `Business Products` → create product → set product variations → create store item (variant) with price & stock → `Website - Cart` (login as customer) → add to cart → checkout → `Business Orders` (login as owner) to manage. --- ## Common Tasks for Contributors ### Add a new API feature 1. Write SQL migration in `database/migrations/` 2. Apply migration, then `npm run prisma:pull && npm run prisma:generate` 3. Create module: `src//` with controller, service, DTOs 4. Register in `app.module.ts` 5. Add permissions to migration if business-scoped 6. Update this document ### Add a business-scoped endpoint 1. Controller: `@Controller('businesses/:businessId/...')` 2. Guards: `@UseGuards(JwtAuthGuard, BusinessPermissionGuard)` 3. Permission: `@RequireBusinessPermission('resource.action')` 4. Service: `assertPermission(businessId, actor.id, 'resource.action')` (defense in depth) ### Add a nested replace resource (like variations) Follow the pattern in `CategoryVariationsService` / `CategoryTechnicalFormService`: - `GET` returns current state - `PUT` validates input, deletes all existing, recreates in a transaction --- ## Related Files | Purpose | Path | |---------|------| | Prisma schema | `prisma/schema.prisma` | | Env template | `.env.example` (`INVOICE_PUBLIC_DOMAIN` / optional `INVOICE_PUBLIC_BASE_URL`) | | Invoices module | `src/invoices/` | | Invoice migrations | `database/migrations/036_invoices.sql` … `039_invoice_account_holder.sql` | | Docker services | `docker-compose.yml` | | Dev seed data | `database/seeds/001_sample_data.sql` | | Postman | `postman/Meshkee-CMS-Auth.postman_collection.json` |