askill
vendix-backend-domain

vendix-backend-domainSafety --Repository

Hexagonal architecture and domain structure.

4 stars
1.2k downloads
Updated 2/24/2026

Package Files

Loading files...
SKILL.md

Vendix Backend Domain Architecture

Backend Domain Pattern - Arquitectura hexagonal basada en dominios con separaciΓ³n clara de responsabilidades.

πŸ—οΈ Domain Architecture

Vendix backend sigue una arquitectura hexagonal basada en dominios, donde cada dominio representa un Γ‘rea de negocio claramente delimitada.

Structure

apps/backend/src/domains/
β”œβ”€β”€ auth/                    # AutenticaciΓ³n y JWT
β”‚   β”œβ”€β”€ auth.module.ts
β”‚   β”œβ”€β”€ auth.controller.ts
β”‚   β”œβ”€β”€ auth.service.ts
β”‚   β”œβ”€β”€ dto/
β”‚   β”‚   β”œβ”€β”€ login.dto.ts
β”‚   β”‚   β”œβ”€β”€ register.dto.ts
β”‚   β”‚   └── refresh-token.dto.ts
β”‚   └── interfaces/
β”‚       └── auth.interface.ts
β”‚
β”œβ”€β”€ organization/            # Organizaciones y usuarios
β”‚   β”œβ”€β”€ organization.module.ts
β”‚   β”œβ”€β”€ organization.controller.ts
β”‚   β”œβ”€β”€ organization.service.ts
β”‚   β”œβ”€β”€ dto/
β”‚   └── interfaces/
β”‚
β”œβ”€β”€ store/                   # Tiendas
β”‚   β”œβ”€β”€ store.module.ts
β”‚   β”œβ”€β”€ store.controller.ts
β”‚   β”œβ”€β”€ store.service.ts
β”‚   β”œβ”€β”€ settings/            # NUEVO: Store settings (branding, fonts, ecommerce)
β”‚   β”‚   β”œβ”€β”€ settings.service.ts
β”‚   β”‚   β”œβ”€β”€ interfaces/
β”‚   β”‚   β”‚   └── store-settings.interface.ts
β”‚   β”‚   └── defaults/
β”‚   β”‚       └── default-store-settings.ts
β”‚   β”œβ”€β”€ products/
β”‚   β”œβ”€β”€ brands/
β”‚   β”œβ”€β”€ categories/
β”‚   └── ecommerce/           # Ecommerce operations
β”‚
β”œβ”€β”€ ecommerce/               # CatΓ‘logo pΓΊblico de e-commerce
β”‚   β”œβ”€β”€ catalog/
β”‚   β”œβ”€β”€ cart/
β”‚   β”œβ”€β”€ checkout/
β”‚   β”œβ”€β”€ wishlist/
β”‚   └── account/
β”‚
β”œβ”€β”€ superadmin/              # AdministraciΓ³n global del sistema
β”‚   β”œβ”€β”€ superadmin.module.ts
β”‚   β”œβ”€β”€ system-management.controller.ts
β”‚   └── system.service.ts
β”‚
β”œβ”€β”€ public/                  # Dominios pΓΊblicos (landing pages)
β”‚   └── domains/
β”‚       β”œβ”€β”€ public-domains.module.ts
β”‚       β”œβ”€β”€ public-domains.controller.ts
β”‚       └── public-domains.service.ts
β”‚
└── common/                  # Utilidades compartidas
    β”œβ”€β”€ middleware/
    β”œβ”€β”€ guards/
    β”œβ”€β”€ decorators/
    β”œβ”€β”€ interceptors/
    β”œβ”€β”€ context/
    β”œβ”€β”€ services/            # S3, helpers, etc.
    └── responses/

πŸ”„ App Type Standard (NEW)

App Type Enum

The backend uses a unified app_type_enum across the entire system:

enum app_type_enum {
  VENDIX_LANDING     # Public: Vendix SaaS landing
  VENDIX_ADMIN       # Private: Super admin panel
  ORG_LANDING        # Public: Organization landing
  ORG_ADMIN          # Private: Organization admin
  STORE_LANDING      # Public: Store landing
  STORE_ADMIN        # Private: Store admin panel
  STORE_ECOMMERCE    # Public: Store e-commerce
}

model domain_settings {
  app_type  app_type_enum  @default(VENDIX_LANDING) // <--- Source of Truth
  config    Json?                                      // Now nullable (legacy)
  // ...
}

model user_settings {
  app_type  app_type_enum  @default(STORE_ADMIN) // Override post-login
  // ...
}

Domain Resolution

File: domains/public/domains/public-domains.service.ts

The resolveDomain() method now returns:

  • app: Direct from domain_settings.app_type (NOT config.app)
  • branding: From store_settings.settings.branding
  • fonts: From store_settings.settings.fonts
  • ecommerce: From store_settings.settings.ecommerce
  • publication: From store_settings.settings.publication
  • config: Legacy (kept for backward compatibility)

metadata: scope: [root] auto_invoke: "Working on backend domains"

πŸ“¦ Domain Module Pattern

Cada dominio sigue este patrΓ³n estΓ‘ndar:

1. Module File

File: {domain}.module.ts

import { Module } from '@nestjs/common';
import { PrismaModule } from '@/prisma/prisma.module';
import { {Domain}Controller } from './{domain}.controller';
import { {Domain}Service } from './{domain}.service';
import { AuthGuard } from '@/common/guards/auth.guard';

@Module({
  imports: [PrismaModule],
  controllers: [{Domain}Controller],
  providers: [
    {Domain}Service,
    // Add domain-specific providers here
  ],
  exports: [{Domain}Service],
})
export class {Domain}Module {}

Rules:

  • Import PrismaModule for database access
  • Export services used by other modules
  • Use dependency injection properly
  • Keep imports minimal

2. Controller File

File: {domain}.controller.ts

import { Controller, Post, Body, Get, UseGuards } from '@nestjs/common';
import { {Domain}Service } from './{domain}.service';
import { {Action}Dto } from './dto/{action}-dto.dto';
import { AuthGuard } from '@/common/guards/auth.guard';
import { Public } from '@/common/decorators/public.decorator';
import { Permissions } from '@/common/decorators/permissions.decorator';

@Controller('domains/:domain_id/{domain}')  // Multi-tenant route
export class {Domain}Controller {
  constructor(
    private readonly {domain}_service: {Domain}Service,
    private readonly response_service: ResponseService,
  ) {}

  @Public()
  @Post('public-action')
  async publicAction(@Body() dto: {Action}Dto) {
    const result = await this.{domain}_service.publicAction(dto);
    return this.response_service.success(result);
  }

  @UseGuards(AuthGuard)
  @Permissions('{domain}:read')
  @Get()
  async findAll() {
    const result = await this.{domain}_service.findAll();
    return this.response_service.success(result);
  }

  @UseGuards(AuthGuard)
  @Permissions('{domain}:write')
  @Post('create')
  async create(@Body() dto: {Action}Dto) {
    const result = await this.{domain}_service.create(dto);
    return this.response_service.success(result, 'Created successfully');
  }
}

Rules:

  • Inject both domain service and ResponseService
  • Use @Public() for public endpoints
  • Use @Permissions() for granular access control
  • Always return through response_service
  • Follow REST conventions

3. Service File

File: {domain}.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '@/prisma/services/prisma.service';
import { RequestContextService } from '@/common/context/request-context.service';

@Injectable()
export class {Domain}Service {
  constructor(
    private readonly prisma: PrismaService,
    private readonly context: RequestContextService,
  ) {}

  async findAll() {
    // Context is automatically applied (organization_id, store_id)
    return this.prisma.{model}.findMany({
      where: {
        organization_id: this.context.organization_id,
        store_id: this.context.store_id,
      },
    });
  }

  async findOne(id: number) {
    const entity = await this.prisma.{model}.findUnique({
      where: { id },
    });

    if (!entity) {
      throw new NotFoundException('{Entity} not found');
    }

    return entity;
  }

  async create(dto: {CreateDto}) {
    // Multi-tenant context is automatic
    return this.prisma.{model}.create({
      data: {
        ...dto,
        organization_id: this.context.organization_id,
        store_id: this.context.store_id,
      },
    });
  }

  async update(id: number, dto: {UpdateDto}) {
    return this.prisma.{model}.update({
      where: { id },
      data: dto,
    });
  }

  async delete(id: number) {
    return this.prisma.{model}.delete({
      where: { id },
    });
  }
}

Rules:

  • Use RequestContextService for multi-tenant context
  • Never hardcode organization_id or store_id
  • Use dependency injection for PrismaService
  • Throw proper exceptions (NotFoundException, etc.)
  • Return early for validation (early return pattern)

πŸ” Authentication & Authorization

Public Routes

import { Public } from '@/common/decorators/public.decorator';

@Public()
@Post('login')
async login() {
  // Accessible without authentication
}

Protected Routes

import { UseGuards } from '@nestjs/common';
import { AuthGuard } from '@/common/guards/auth.guard';

@UseGuards(AuthGuard)
@Get('profile')
async getProfile() {
  // Requires valid JWT
}

Role-Based Access

import { Roles } from '@/common/decorators/roles.decorator';

@Roles('super_admin')
@Get('admin-only')
async adminOnly() {
  // Only super_admin role
}

Permission-Based Access

import { Permissions } from '@/common/decorators/permissions.decorator';

@Permissions('catalog:write')
@Post('products')
async createProduct() {
  // Requires catalog:write permission
}

🌐 Multi-Tenancy

Automatic Context Injection

From: app.module.ts

providers: [
  {
    provide: APP_GUARD,
    useClass: AuthGuard,
  },
  RequestContextService,  // Automatic multi-tenant context
],

Usage in Services

constructor(
  private readonly context: RequestContextService,
) {}

async createProduct(dto: CreateProductDto) {
  return this.prisma.product.create({
    data: {
      ...dto,
      organization_id: this.context.organization_id,  // Auto-injected
      store_id: this.context.store_id,                // Auto-injected
    },
  });
}

Never bypass the context! Always use this.context for tenant IDs.


πŸ“ DTOs (Data Transfer Objects)

Create DTO

// dto/create-{entity}.dto.ts
import { IsString, IsEmail, IsOptional, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(3)
  user_name: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;

  @IsOptional()
  @IsString()
  phone_number?: string;
}

Update DTO

// dto/update-{entity}.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto) {}

Query DTO

// dto/query-{entity}.dto.ts
import { IsOptional, IsString, IsNumber } from 'class-validator';
import { Type } from 'class-transformer';

export class QueryUserDto {
  @IsOptional()
  @IsString()
  search?: string;

  @IsOptional()
  @Type(() => Number)
  @IsNumber()
  page?: number = 1;

  @IsOptional()
  @Type(() => Number)
  @IsNumber()
  limit?: number = 10;
}

🎯 Domain Routing

Multi-Tenant Routes

@Controller('domains/:domain_id/organizations/:organization_id/{resource}')
export class ResourceController {
  // Routes include domain and organization for multi-tenancy
}

Store-Level Routes

@Controller('domains/:domain_id/stores/:store_id/{resource}')
export class StoreResourceController {
  // Store-scoped routes
}

πŸ“‹ Common Patterns

Error Handling

async findUser(id: number) {
  // Early return for validation
  if (!id || id <= 0) {
    throw new BadRequestException('Invalid ID');
  }

  try {
    const user = await this.prisma.users.findUnique({ where: { id } });

    if (!user) {
      throw new NotFoundException('User not found');
    }

    return user;
  } catch (error) {
    // Handle Prisma errors
    if (error.code === 'P2002') {
      throw new ConflictException('User already exists');
    }
    throw error;
  }
}

Pagination

async findAll(query: QueryDto) {
  const page = query.page || 1;
  const limit = query.limit || 10;
  const skip = (page - 1) * limit;

  const [data, total] = await Promise.all([
    this.prisma.resource.findMany({
      skip,
      take: limit,
      where: this.buildWhereClause(query),
    }),
    this.prisma.resource.count({ where: this.buildWhereClause(query) }),
  ]);

  return {
    data,
    meta: {
      total,
      page,
      limit,
      total_pages: Math.ceil(total / limit),
    },
  };
}

Soft Delete

async delete(id: number) {
  return this.prisma.resource.update({
    where: { id },
    data: {
      deleted_at: new Date(),
      is_active: false,
    },
  });
}

async findActive() {
  return this.prisma.resource.findMany({
    where: {
      deleted_at: null,
      is_active: true,
    },
  });
}

πŸ” Key Files Reference

FilePurpose
app.module.tsRoot module with global guards and context
common/guards/auth.guard.tsJWT authentication guard
common/decorators/public.decorator.tsMark public routes
common/decorators/permissions.decorator.tsPermission-based access
common/context/request-context.service.tsMulti-tenant context
common/responses/response.service.tsStandardized responses
domains/public/domains/public-domains.service.tsDomain resolution with app_type
domains/store/settings/interfaces/store-settings.interface.tsStore settings interfaces
domains/store/settings/defaults/default-store-settings.tsDefault store settings

πŸ“ App Type Migration Notes

Before (Legacy):

// Old way - config.app nested in config JSON
config: {
  app: 'STORE_ADMIN',
  branding: { ... }
}

After (New Standard):

// New way - app_type directly on domain
app_type: 'STORE_ADMIN'  // Source of Truth

// Branding from store_settings.settings.branding
branding: {
  name: string;
  primary_color: string;
  // ...
}

Related Skills

  • vendix-backend-prisma - Prisma service patterns
  • vendix-backend-auth - JWT and authorization patterns
  • vendix-backend-middleware - Middleware and domain resolution
  • vendix-naming-conventions - Naming conventions (CRITICAL)

Install

Download ZIP
Requires askill CLI v1.0+β–Ά

AI Quality Score

AI review pending.

Metadata

Licenseunknown
Version-
Updated2/24/2026
PublisherRzyfront

Tags

apidatabasesecurity