Files
backend/docs/PROJECT_CONTEXT.md
T
Alireza HassaniandCursor 3faeb9bc0d Expand invoices with full templates, public viewer API, and account holder.
Add invoice template CRUD, key points/accounts, public GET endpoint, and migration 039 for account_holder_name.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 11:04:26 +03:30

24 KiB

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.tsdto/


Getting Started

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/<resource>

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

@UseGuards(JwtAuthGuard, BusinessPermissionGuard)
@RequireBusinessPermission('products.read')
  • JwtAuthGuard — validates Bearer JWT (type: 'access')
  • BusinessPermissionGuard — checks permission for businessId route param
  • PermissionsServiceisSuperAdmin(), 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:

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:

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:

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:

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)

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/<feature>/ 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

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.sql039_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