# ColaFlow M1 Architecture Design **Version:** 1.0 **Date:** 2025-11-02 **Milestone:** M1 - Core Project Management Module **Duration:** 8 weeks (Sprints 1-4) --- ## Executive Summary This document defines the complete system architecture for ColaFlow M1, implementing the Core Project Management Module. The architecture leverages modern technologies: **.NET 9** with **Domain-Driven Design (DDD)** for the backend, **PostgreSQL** for data persistence, **React 19 + Next.js 15** for the frontend, and **REST + SignalR** for API communication. ### Key Architecture Decisions | Decision | Technology | Rationale | |----------|-----------|-----------| | **Backend Framework** | .NET 9 with Clean Architecture | Enterprise-grade, excellent DDD support, strong typing | | **Architectural Pattern** | DDD + CQRS + Event Sourcing | Domain complexity, audit trail requirements, scalability | | **Primary Database** | PostgreSQL 16+ | ACID transactions, JSONB flexibility, excellent for hierarchies | | **Caching** | Redis 7+ | Session management, real-time scaling, pub/sub | | **Frontend Framework** | React 19 + Next.js 15 | Largest community, excellent DX, SSR/SSG capabilities | | **UI Components** | shadcn/ui + Radix UI + Tailwind | Component ownership, accessibility, modern design | | **State Management** | TanStack Query + Zustand | Server state + client state separation | | **API Protocol** | REST with OpenAPI 3.1 | Broad compatibility, excellent tooling, MCP-ready | | **Real-time** | SignalR (WebSockets/HTTP2) | Native .NET integration, automatic fallback | --- ## 1. System Architecture Overview ### 1.1 High-Level Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ CLIENT LAYER │ │ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐ │ │ │ Web Browser │ │ Desktop App │ │ Mobile App │ │ │ │ (Next.js 15) │ │ (Future) │ │ (Future) │ │ │ └────────┬─────────┘ └────────┬─────────┘ └───────┬───────┘ │ └───────────┼──────────────────────┼─────────────────────┼─────────┘ │ │ │ │ HTTPS / WSS │ │ └──────────────────────┴─────────────────────┘ │ ┌─────────────────────────────────────────────────────────────────┐ │ API GATEWAY │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ ASP.NET Core 9 API Gateway (Future M2+) │ │ │ │ - Rate Limiting - Authentication - Routing │ │ │ └──────────────────────────────────────────────────────────┘ │ └───────────────────────────────┬─────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────────────┐ │ APPLICATION LAYER │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ ColaFlow.API (.NET 9) │ │ │ │ ┌────────────────┐ ┌─────────────┐ ┌────────────────┐ │ │ │ │ │ REST API │ │ SignalR │ │ OpenAPI │ │ │ │ │ │ Controllers │ │ Hubs │ │ Documentation │ │ │ │ │ └────────────────┘ └─────────────┘ └────────────────┘ │ │ │ │ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ │ APPLICATION SERVICES LAYER │ │ │ │ │ │ (CQRS - Commands & Queries via MediatR) │ │ │ │ │ │ - Project Management - User Management │ │ │ │ │ │ - Workflow Engine - Audit Logging │ │ │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ │ DOMAIN LAYER │ │ │ │ │ │ (Business Logic - Framework Independent) │ │ │ │ │ │ - Aggregates - Entities - Value Objects │ │ │ │ │ │ - Domain Events - Domain Services │ │ │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ │ INFRASTRUCTURE LAYER │ │ │ │ │ │ - EF Core Repositories - Event Store │ │ │ │ │ │ - External Services - Cross-cutting Concerns │ │ │ │ │ └────────────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────┘ │ └───────────────────────────────┬─────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────────────┐ │ DATA LAYER │ │ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐ │ │ │ PostgreSQL 16 │ │ Redis 7 │ │ File Storage │ │ │ │ (Primary DB) │ │ (Cache/Session) │ │ (Attachments)│ │ │ │ - Transactional │ │ - SignalR Backpl│ │ - MinIO/S3 │ │ │ │ - Event Store │ │ - Rate Limiting │ │ (Future) │ │ │ │ - JSONB Support │ │ - Pub/Sub │ │ │ │ │ └──────────────────┘ └──────────────────┘ └───────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 1.2 Architecture Principles 1. **Clean Architecture**: Separation of concerns with dependency inversion 2. **Domain-Driven Design**: Rich domain model, ubiquitous language 3. **CQRS**: Separate read and write models for performance 4. **Event Sourcing**: Complete audit trail via domain events 5. **API-First**: OpenAPI specification drives implementation 6. **Testability**: All layers independently testable 7. **Scalability**: Stateless services, horizontal scaling ready --- ## 2. Backend Architecture (.NET 9 with DDD) ### 2.1 Clean Architecture Layers ``` ColaFlow.sln │ ├── src/ │ ├── ColaFlow.Domain/ # Core business logic (innermost layer) │ │ ├── Aggregates/ │ │ │ ├── ProjectAggregate/ │ │ │ │ ├── Project.cs # Aggregate Root │ │ │ │ ├── Epic.cs # Entity │ │ │ │ ├── Story.cs # Entity │ │ │ │ ├── Task.cs # Entity │ │ │ │ └── Subtask.cs # Entity │ │ │ ├── UserAggregate/ │ │ │ └── WorkflowAggregate/ │ │ ├── ValueObjects/ │ │ │ ├── ProjectId.cs │ │ │ ├── TaskPriority.cs │ │ │ ├── TaskStatus.cs │ │ │ └── CustomField.cs │ │ ├── DomainEvents/ │ │ │ ├── ProjectCreatedEvent.cs │ │ │ ├── TaskStatusChangedEvent.cs │ │ │ └── TaskAssignedEvent.cs │ │ ├── Interfaces/ │ │ │ ├── IProjectRepository.cs │ │ │ └── IUnitOfWork.cs │ │ ├── Services/ │ │ │ └── WorkflowService.cs # Domain Service │ │ └── Exceptions/ │ │ └── DomainException.cs │ │ │ ├── ColaFlow.Application/ # Use cases and orchestration │ │ ├── Commands/ # CQRS Commands │ │ │ ├── Projects/ │ │ │ │ ├── CreateProject/ │ │ │ │ │ ├── CreateProjectCommand.cs │ │ │ │ │ ├── CreateProjectCommandHandler.cs │ │ │ │ │ └── CreateProjectCommandValidator.cs │ │ │ │ ├── UpdateProject/ │ │ │ │ └── DeleteProject/ │ │ │ ├── Tasks/ │ │ │ │ ├── CreateTask/ │ │ │ │ ├── UpdateTaskStatus/ │ │ │ │ └── AssignTask/ │ │ │ └── Workflows/ │ │ ├── Queries/ # CQRS Queries │ │ │ ├── Projects/ │ │ │ │ ├── GetProjectById/ │ │ │ │ │ ├── GetProjectByIdQuery.cs │ │ │ │ │ └── GetProjectByIdQueryHandler.cs │ │ │ │ ├── GetProjectList/ │ │ │ │ └── GetProjectKanban/ │ │ │ ├── Tasks/ │ │ │ └── AuditLogs/ │ │ ├── DTOs/ │ │ │ ├── ProjectDto.cs │ │ │ ├── TaskDto.cs │ │ │ └── KanbanBoardDto.cs │ │ ├── Mappings/ │ │ │ └── AutoMapperProfile.cs │ │ ├── Behaviors/ # MediatR Pipeline Behaviors │ │ │ ├── ValidationBehavior.cs │ │ │ ├── LoggingBehavior.cs │ │ │ └── TransactionBehavior.cs │ │ └── Interfaces/ │ │ └── ICurrentUserService.cs │ │ │ ├── ColaFlow.Infrastructure/ # External concerns │ │ ├── Persistence/ │ │ │ ├── ColaFlowDbContext.cs # EF Core DbContext │ │ │ ├── Configurations/ # Entity Configurations │ │ │ │ ├── ProjectConfiguration.cs │ │ │ │ └── TaskConfiguration.cs │ │ │ ├── Repositories/ │ │ │ │ ├── ProjectRepository.cs │ │ │ │ └── UserRepository.cs │ │ │ ├── Migrations/ │ │ │ └── EventStore/ │ │ │ └── EventStoreRepository.cs │ │ ├── Identity/ │ │ │ ├── IdentityService.cs │ │ │ └── CurrentUserService.cs │ │ ├── Services/ │ │ │ ├── DateTimeService.cs │ │ │ └── EmailService.cs │ │ ├── Caching/ │ │ │ └── RedisCacheService.cs │ │ └── SignalR/ │ │ └── NotificationHub.cs │ │ │ └── ColaFlow.API/ # Presentation layer │ ├── Controllers/ │ │ ├── ProjectsController.cs │ │ ├── TasksController.cs │ │ ├── WorkflowsController.cs │ │ └── AuditLogsController.cs │ ├── Hubs/ │ │ └── ProjectHub.cs # SignalR Hub │ ├── Middleware/ │ │ ├── ExceptionHandlingMiddleware.cs │ │ └── RequestLoggingMiddleware.cs │ ├── Filters/ │ │ ├── ValidateModelStateFilter.cs │ │ └── ApiExceptionFilter.cs │ ├── Program.cs # Application entry point │ ├── appsettings.json │ └── appsettings.Development.json │ └── tests/ ├── ColaFlow.Domain.Tests/ # Domain unit tests ├── ColaFlow.Application.Tests/ # Application unit tests ├── ColaFlow.Infrastructure.Tests/ # Infrastructure integration tests └── ColaFlow.API.Tests/ # API integration tests ``` ### 2.2 Domain Layer - DDD Tactical Patterns #### 2.2.1 Project Aggregate Root ```csharp namespace ColaFlow.Domain.Aggregates.ProjectAggregate { /// /// Project Aggregate Root /// Enforces consistency boundary for Project -> Epic -> Story -> Task hierarchy /// public class Project : AggregateRoot { public ProjectId Id { get; private set; } public string Name { get; private set; } public string Description { get; private set; } public ProjectKey Key { get; private set; } // e.g., "COLA" public ProjectStatus Status { get; private set; } public UserId OwnerId { get; private set; } private readonly List _epics = new(); public IReadOnlyCollection Epics => _epics.AsReadOnly(); public DateTime CreatedAt { get; private set; } public DateTime? UpdatedAt { get; private set; } // Factory method public static Project Create(string name, string description, string key, UserId ownerId) { // Validation if (string.IsNullOrWhiteSpace(name)) throw new DomainException("Project name cannot be empty"); if (key.Length > 10) throw new DomainException("Project key cannot exceed 10 characters"); var project = new Project { Id = ProjectId.Create(), Name = name, Description = description, Key = ProjectKey.Create(key), Status = ProjectStatus.Active, OwnerId = ownerId, CreatedAt = DateTime.UtcNow }; // Raise domain event project.AddDomainEvent(new ProjectCreatedEvent(project.Id, project.Name, ownerId)); return project; } // Business methods public void UpdateDetails(string name, string description) { if (string.IsNullOrWhiteSpace(name)) throw new DomainException("Project name cannot be empty"); Name = name; Description = description; UpdatedAt = DateTime.UtcNow; AddDomainEvent(new ProjectUpdatedEvent(Id, Name, Description)); } public Epic CreateEpic(string name, string description, UserId createdBy) { var epic = Epic.Create(name, description, this.Id, createdBy); _epics.Add(epic); AddDomainEvent(new EpicCreatedEvent(epic.Id, epic.Name, this.Id)); return epic; } public void Archive() { if (Status == ProjectStatus.Archived) throw new DomainException("Project is already archived"); Status = ProjectStatus.Archived; UpdatedAt = DateTime.UtcNow; AddDomainEvent(new ProjectArchivedEvent(Id)); } } /// /// Epic Entity (part of Project aggregate) /// public class Epic : Entity { public EpicId Id { get; private set; } public string Name { get; private set; } public string Description { get; private set; } public ProjectId ProjectId { get; private set; } public TaskStatus Status { get; private set; } private readonly List _stories = new(); public IReadOnlyCollection Stories => _stories.AsReadOnly(); public DateTime CreatedAt { get; private set; } public UserId CreatedBy { get; private set; } public static Epic Create(string name, string description, ProjectId projectId, UserId createdBy) { return new Epic { Id = EpicId.Create(), Name = name, Description = description, ProjectId = projectId, Status = TaskStatus.ToDo, CreatedAt = DateTime.UtcNow, CreatedBy = createdBy }; } public Story CreateStory(string title, string description, TaskPriority priority, UserId createdBy) { var story = Story.Create(title, description, this.Id, priority, createdBy); _stories.Add(story); return story; } } // Story and Task entities follow similar patterns... } ``` #### 2.2.2 Value Objects ```csharp namespace ColaFlow.Domain.ValueObjects { /// /// ProjectId Value Object (strongly-typed ID) /// public sealed class ProjectId : ValueObject { public Guid Value { get; private set; } private ProjectId(Guid value) { Value = value; } public static ProjectId Create() => new ProjectId(Guid.NewGuid()); public static ProjectId Create(Guid value) => new ProjectId(value); protected override IEnumerable GetAtomicValues() { yield return Value; } public override string ToString() => Value.ToString(); } /// /// TaskPriority Value Object (enumeration) /// public sealed class TaskPriority : Enumeration { public static readonly TaskPriority Low = new(1, "Low"); public static readonly TaskPriority Medium = new(2, "Medium"); public static readonly TaskPriority High = new(3, "High"); public static readonly TaskPriority Urgent = new(4, "Urgent"); private TaskPriority(int id, string name) : base(id, name) { } } /// /// CustomField Value Object (flexible schema) /// public sealed class CustomField : ValueObject { public string Key { get; private set; } public string Value { get; private set; } public CustomFieldType Type { get; private set; } public static CustomField Create(string key, string value, CustomFieldType type) { if (string.IsNullOrWhiteSpace(key)) throw new DomainException("Custom field key cannot be empty"); return new CustomField { Key = key, Value = value, Type = type }; } protected override IEnumerable GetAtomicValues() { yield return Key; yield return Value; yield return Type; } } } ``` #### 2.2.3 Domain Events ```csharp namespace ColaFlow.Domain.DomainEvents { /// /// Base Domain Event /// public abstract record DomainEvent { public Guid EventId { get; init; } = Guid.NewGuid(); public DateTime OccurredOn { get; init; } = DateTime.UtcNow; } /// /// ProjectCreatedEvent /// public record ProjectCreatedEvent( ProjectId ProjectId, string ProjectName, UserId CreatedBy ) : DomainEvent; /// /// TaskStatusChangedEvent (for audit trail and real-time notifications) /// public record TaskStatusChangedEvent( TaskId TaskId, TaskStatus OldStatus, TaskStatus NewStatus, UserId ChangedBy, string Reason ) : DomainEvent; /// /// TaskAssignedEvent /// public record TaskAssignedEvent( TaskId TaskId, UserId AssigneeId, UserId AssignedBy ) : DomainEvent; } ``` ### 2.3 Application Layer - CQRS with MediatR #### 2.3.1 Command Example ```csharp namespace ColaFlow.Application.Commands.Projects.CreateProject { // Command (request) public sealed record CreateProjectCommand : IRequest { public string Name { get; init; } = string.Empty; public string Description { get; init; } = string.Empty; public string Key { get; init; } = string.Empty; } // Command Validator (FluentValidation) public sealed class CreateProjectCommandValidator : AbstractValidator { public CreateProjectCommandValidator() { RuleFor(x => x.Name) .NotEmpty().WithMessage("Project name is required") .MaximumLength(200).WithMessage("Project name cannot exceed 200 characters"); RuleFor(x => x.Key) .NotEmpty().WithMessage("Project key is required") .Matches("^[A-Z]{2,10}$").WithMessage("Project key must be 2-10 uppercase letters"); } } // Command Handler public sealed class CreateProjectCommandHandler : IRequestHandler { private readonly IProjectRepository _projectRepository; private readonly IUnitOfWork _unitOfWork; private readonly ICurrentUserService _currentUserService; private readonly IMapper _mapper; private readonly ILogger _logger; public CreateProjectCommandHandler( IProjectRepository projectRepository, IUnitOfWork unitOfWork, ICurrentUserService currentUserService, IMapper mapper, ILogger logger) { _projectRepository = projectRepository; _unitOfWork = unitOfWork; _currentUserService = currentUserService; _mapper = mapper; _logger = logger; } public async Task Handle(CreateProjectCommand request, CancellationToken cancellationToken) { _logger.LogInformation("Creating project: {ProjectName}", request.Name); // Check if project key already exists var existingProject = await _projectRepository.GetByKeyAsync(request.Key, cancellationToken); if (existingProject != null) throw new DomainException($"Project with key '{request.Key}' already exists"); // Get current user var currentUserId = UserId.Create(_currentUserService.UserId); // Create aggregate var project = Project.Create( request.Name, request.Description, request.Key, currentUserId ); // Save to repository await _projectRepository.AddAsync(project, cancellationToken); await _unitOfWork.CommitAsync(cancellationToken); _logger.LogInformation("Project created successfully: {ProjectId}", project.Id); // Return DTO return _mapper.Map(project); } } } ``` #### 2.3.2 Query Example ```csharp namespace ColaFlow.Application.Queries.Projects.GetProjectKanban { // Query (request) public sealed record GetProjectKanbanQuery(Guid ProjectId) : IRequest; // Query Handler (can use Dapper for performance) public sealed class GetProjectKanbanQueryHandler : IRequestHandler { private readonly ColaFlowDbContext _context; private readonly IMapper _mapper; public GetProjectKanbanQueryHandler(ColaFlowDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } public async Task Handle(GetProjectKanbanQuery request, CancellationToken cancellationToken) { // Optimized query for read side var project = await _context.Projects .AsNoTracking() .Include(p => p.Epics) .ThenInclude(e => e.Stories) .ThenInclude(s => s.Tasks) .FirstOrDefaultAsync(p => p.Id == ProjectId.Create(request.ProjectId), cancellationToken); if (project == null) throw new NotFoundException(nameof(Project), request.ProjectId); // Transform to Kanban view model var kanban = new KanbanBoardDto { ProjectId = project.Id.Value, ProjectName = project.Name, Columns = new List { new() { Status = "To Do", Tasks = GetTasksByStatus(project, TaskStatus.ToDo) }, new() { Status = "In Progress", Tasks = GetTasksByStatus(project, TaskStatus.InProgress) }, new() { Status = "Review", Tasks = GetTasksByStatus(project, TaskStatus.InReview) }, new() { Status = "Done", Tasks = GetTasksByStatus(project, TaskStatus.Done) } } }; return kanban; } private List GetTasksByStatus(Project project, TaskStatus status) { return project.Epics .SelectMany(e => e.Stories) .SelectMany(s => s.Tasks) .Where(t => t.Status == status) .Select(t => _mapper.Map(t)) .ToList(); } } } ``` #### 2.3.3 MediatR Pipeline Behaviors ```csharp namespace ColaFlow.Application.Behaviors { /// /// Validation Behavior (runs before handler) /// public sealed class ValidationBehavior : IPipelineBehavior where TRequest : IRequest { private readonly IEnumerable> _validators; public ValidationBehavior(IEnumerable> validators) { _validators = validators; } public async Task Handle( TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { if (!_validators.Any()) return await next(); var context = new ValidationContext(request); var validationResults = await Task.WhenAll( _validators.Select(v => v.ValidateAsync(context, cancellationToken))); var failures = validationResults .SelectMany(r => r.Errors) .Where(f => f != null) .ToList(); if (failures.Any()) throw new ValidationException(failures); return await next(); } } /// /// Transaction Behavior (commits UnitOfWork after handler) /// public sealed class TransactionBehavior : IPipelineBehavior where TRequest : IRequest { private readonly IUnitOfWork _unitOfWork; public TransactionBehavior(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } public async Task Handle( TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { // Execute handler var response = await next(); // Commit transaction and dispatch domain events await _unitOfWork.CommitAsync(cancellationToken); return response; } } } ``` ### 2.4 Infrastructure Layer - Persistence #### 2.4.1 EF Core DbContext ```csharp namespace ColaFlow.Infrastructure.Persistence { public class ColaFlowDbContext : DbContext, IUnitOfWork { private readonly IDomainEventDispatcher _domainEventDispatcher; public DbSet Projects => Set(); public DbSet Users => Set(); public DbSet Workflows => Set(); public DbSet AuditLogs => Set(); public DbSet DomainEvents => Set(); public ColaFlowDbContext( DbContextOptions options, IDomainEventDispatcher domainEventDispatcher) : base(options) { _domainEventDispatcher = domainEventDispatcher; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); // Global query filters modelBuilder.Entity().HasQueryFilter(p => !p.IsDeleted); modelBuilder.Entity().HasQueryFilter(u => !u.IsDeleted); } public async Task CommitAsync(CancellationToken cancellationToken = default) { // Dispatch domain events before saving await DispatchDomainEventsAsync(cancellationToken); // Save changes return await base.SaveChangesAsync(cancellationToken); } private async Task DispatchDomainEventsAsync(CancellationToken cancellationToken) { var domainEntities = ChangeTracker .Entries() .Where(x => x.Entity.DomainEvents.Any()) .Select(x => x.Entity) .ToList(); var domainEvents = domainEntities .SelectMany(x => x.DomainEvents) .ToList(); // Clear domain events domainEntities.ForEach(entity => entity.ClearDomainEvents()); // Dispatch events foreach (var domainEvent in domainEvents) { await _domainEventDispatcher.DispatchAsync(domainEvent, cancellationToken); } // Store events in event store foreach (var domainEvent in domainEvents) { DomainEvents.Add(new DomainEventRecord { Id = Guid.NewGuid(), EventType = domainEvent.GetType().Name, EventData = JsonSerializer.Serialize(domainEvent), OccurredOn = domainEvent.OccurredOn }); } } } } ``` #### 2.4.2 Entity Configuration (Fluent API) ```csharp namespace ColaFlow.Infrastructure.Persistence.Configurations { public class ProjectConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("Projects"); builder.HasKey(p => p.Id); builder.Property(p => p.Id) .HasConversion( id => id.Value, value => ProjectId.Create(value)) .IsRequired(); builder.Property(p => p.Name) .IsRequired() .HasMaxLength(200); builder.Property(p => p.Description) .HasMaxLength(2000); builder.OwnsOne(p => p.Key, keyBuilder => { keyBuilder.Property(k => k.Value) .HasColumnName("Key") .IsRequired() .HasMaxLength(10); keyBuilder.HasIndex(k => k.Value).IsUnique(); }); builder.Property(p => p.Status) .HasConversion() .IsRequired(); // Owned collection - Custom Fields (stored as JSONB in PostgreSQL) builder.OwnsMany(p => p.CustomFields, cfBuilder => { cfBuilder.ToJson("CustomFieldsJson"); }); // Navigation properties builder.HasMany(p => p.Epics) .WithOne() .HasForeignKey("ProjectId") .OnDelete(DeleteBehavior.Cascade); builder.HasOne() .WithMany() .HasForeignKey(p => p.OwnerId) .OnDelete(DeleteBehavior.Restrict); // Indexes builder.HasIndex(p => p.CreatedAt); builder.HasIndex(p => p.Status); // Soft delete builder.Property("IsDeleted").HasDefaultValue(false); builder.Property("DeletedAt"); } } } ``` #### 2.4.3 Repository Implementation ```csharp namespace ColaFlow.Infrastructure.Persistence.Repositories { public class ProjectRepository : IProjectRepository { private readonly ColaFlowDbContext _context; public ProjectRepository(ColaFlowDbContext context) { _context = context; } public async Task GetByIdAsync(ProjectId id, CancellationToken cancellationToken = default) { return await _context.Projects .Include(p => p.Epics) .ThenInclude(e => e.Stories) .ThenInclude(s => s.Tasks) .FirstOrDefaultAsync(p => p.Id == id, cancellationToken); } public async Task GetByKeyAsync(string key, CancellationToken cancellationToken = default) { return await _context.Projects .FirstOrDefaultAsync(p => p.Key.Value == key, cancellationToken); } public async Task> GetAllAsync(int page, int pageSize, CancellationToken cancellationToken = default) { return await _context.Projects .OrderByDescending(p => p.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(cancellationToken); } public async Task AddAsync(Project project, CancellationToken cancellationToken = default) { await _context.Projects.AddAsync(project, cancellationToken); } public void Update(Project project) { _context.Projects.Update(project); } public void Delete(Project project) { // Soft delete _context.Entry(project).Property("IsDeleted").CurrentValue = true; _context.Entry(project).Property("DeletedAt").CurrentValue = DateTime.UtcNow; } } } ``` --- ## 3. Database Design (PostgreSQL) ### 3.1 Database Schema ```sql -- Projects Table CREATE TABLE "Projects" ( "Id" UUID PRIMARY KEY, "Name" VARCHAR(200) NOT NULL, "Description" VARCHAR(2000), "Key" VARCHAR(10) NOT NULL UNIQUE, "Status" VARCHAR(50) NOT NULL, "OwnerId" UUID NOT NULL, "CustomFieldsJson" JSONB, "CreatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "UpdatedAt" TIMESTAMP, "IsDeleted" BOOLEAN NOT NULL DEFAULT FALSE, "DeletedAt" TIMESTAMP, CONSTRAINT "FK_Projects_Users" FOREIGN KEY ("OwnerId") REFERENCES "Users"("Id") ); CREATE INDEX "IX_Projects_Key" ON "Projects"("Key"); CREATE INDEX "IX_Projects_Status" ON "Projects"("Status"); CREATE INDEX "IX_Projects_CreatedAt" ON "Projects"("CreatedAt"); CREATE INDEX "IX_Projects_OwnerId" ON "Projects"("OwnerId"); -- Epics Table CREATE TABLE "Epics" ( "Id" UUID PRIMARY KEY, "Name" VARCHAR(200) NOT NULL, "Description" VARCHAR(2000), "ProjectId" UUID NOT NULL, "Status" VARCHAR(50) NOT NULL, "Priority" VARCHAR(50) NOT NULL DEFAULT 'Medium', "CreatedBy" UUID NOT NULL, "CreatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "UpdatedAt" TIMESTAMP, "IsDeleted" BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT "FK_Epics_Projects" FOREIGN KEY ("ProjectId") REFERENCES "Projects"("Id") ON DELETE CASCADE, CONSTRAINT "FK_Epics_Users" FOREIGN KEY ("CreatedBy") REFERENCES "Users"("Id") ); CREATE INDEX "IX_Epics_ProjectId" ON "Epics"("ProjectId"); CREATE INDEX "IX_Epics_Status" ON "Epics"("Status"); -- Stories Table CREATE TABLE "Stories" ( "Id" UUID PRIMARY KEY, "Title" VARCHAR(200) NOT NULL, "Description" TEXT, "EpicId" UUID NOT NULL, "Status" VARCHAR(50) NOT NULL, "Priority" VARCHAR(50) NOT NULL DEFAULT 'Medium', "EstimatedHours" DECIMAL(10,2), "ActualHours" DECIMAL(10,2), "AssigneeId" UUID, "CreatedBy" UUID NOT NULL, "CreatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "UpdatedAt" TIMESTAMP, "IsDeleted" BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT "FK_Stories_Epics" FOREIGN KEY ("EpicId") REFERENCES "Epics"("Id") ON DELETE CASCADE, CONSTRAINT "FK_Stories_AssigneeId" FOREIGN KEY ("AssigneeId") REFERENCES "Users"("Id"), CONSTRAINT "FK_Stories_CreatedBy" FOREIGN KEY ("CreatedBy") REFERENCES "Users"("Id") ); CREATE INDEX "IX_Stories_EpicId" ON "Stories"("EpicId"); CREATE INDEX "IX_Stories_Status" ON "Stories"("Status"); CREATE INDEX "IX_Stories_AssigneeId" ON "Stories"("AssigneeId"); -- Tasks Table CREATE TABLE "Tasks" ( "Id" UUID PRIMARY KEY, "Title" VARCHAR(200) NOT NULL, "Description" TEXT, "StoryId" UUID NOT NULL, "Status" VARCHAR(50) NOT NULL, "Priority" VARCHAR(50) NOT NULL DEFAULT 'Medium', "EstimatedHours" DECIMAL(10,2), "ActualHours" DECIMAL(10,2), "AssigneeId" UUID, "CustomFieldsJson" JSONB, "CreatedBy" UUID NOT NULL, "CreatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "UpdatedAt" TIMESTAMP, "IsDeleted" BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT "FK_Tasks_Stories" FOREIGN KEY ("StoryId") REFERENCES "Stories"("Id") ON DELETE CASCADE, CONSTRAINT "FK_Tasks_AssigneeId" FOREIGN KEY ("AssigneeId") REFERENCES "Users"("Id"), CONSTRAINT "FK_Tasks_CreatedBy" FOREIGN KEY ("CreatedBy") REFERENCES "Users"("Id") ); CREATE INDEX "IX_Tasks_StoryId" ON "Tasks"("StoryId"); CREATE INDEX "IX_Tasks_Status" ON "Tasks"("Status"); CREATE INDEX "IX_Tasks_AssigneeId" ON "Tasks"("AssigneeId"); CREATE INDEX "IX_Tasks_Priority" ON "Tasks"("Priority"); -- Users Table CREATE TABLE "Users" ( "Id" UUID PRIMARY KEY, "Email" VARCHAR(255) NOT NULL UNIQUE, "FirstName" VARCHAR(100) NOT NULL, "LastName" VARCHAR(100) NOT NULL, "PasswordHash" VARCHAR(500) NOT NULL, "Role" VARCHAR(50) NOT NULL DEFAULT 'User', "CreatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "UpdatedAt" TIMESTAMP, "IsDeleted" BOOLEAN NOT NULL DEFAULT FALSE ); CREATE INDEX "IX_Users_Email" ON "Users"("Email"); -- Workflows Table CREATE TABLE "Workflows" ( "Id" UUID PRIMARY KEY, "Name" VARCHAR(200) NOT NULL, "ProjectId" UUID NOT NULL, "IsDefault" BOOLEAN NOT NULL DEFAULT FALSE, "StatusesJson" JSONB NOT NULL, -- Array of statuses with transitions "CreatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "UpdatedAt" TIMESTAMP, CONSTRAINT "FK_Workflows_Projects" FOREIGN KEY ("ProjectId") REFERENCES "Projects"("Id") ON DELETE CASCADE ); CREATE INDEX "IX_Workflows_ProjectId" ON "Workflows"("ProjectId"); -- Audit Logs Table (Event Store) CREATE TABLE "AuditLogs" ( "Id" BIGSERIAL PRIMARY KEY, "EntityType" VARCHAR(100) NOT NULL, "EntityId" UUID NOT NULL, "Action" VARCHAR(100) NOT NULL, "Changes" JSONB NOT NULL, "UserId" UUID NOT NULL, "Timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "IpAddress" VARCHAR(50), CONSTRAINT "FK_AuditLogs_Users" FOREIGN KEY ("UserId") REFERENCES "Users"("Id") ); CREATE INDEX "IX_AuditLogs_EntityType_EntityId" ON "AuditLogs"("EntityType", "EntityId"); CREATE INDEX "IX_AuditLogs_Timestamp" ON "AuditLogs"("Timestamp" DESC); CREATE INDEX "IX_AuditLogs_UserId" ON "AuditLogs"("UserId"); -- Domain Events Table (Event Store) CREATE TABLE "DomainEvents" ( "Id" BIGSERIAL PRIMARY KEY, "EventType" VARCHAR(200) NOT NULL, "AggregateId" UUID NOT NULL, "EventData" JSONB NOT NULL, "OccurredOn" TIMESTAMP NOT NULL, "ProcessedOn" TIMESTAMP ); CREATE INDEX "IX_DomainEvents_AggregateId" ON "DomainEvents"("AggregateId"); CREATE INDEX "IX_DomainEvents_EventType" ON "DomainEvents"("EventType"); CREATE INDEX "IX_DomainEvents_OccurredOn" ON "DomainEvents"("OccurredOn" DESC); ``` ### 3.2 Database Strategy Decisions #### Why PostgreSQL? 1. **ACID Transactions**: Essential for DDD aggregate consistency 2. **JSONB Support**: Flexible schema for custom fields without schema migrations 3. **Recursive Queries**: Excellent for hierarchical data (Projects → Epics → Stories → Tasks) 4. **Full-Text Search**: Built-in search capabilities 5. **Event Sourcing**: Perfect for audit logs and event store 6. **Performance**: Fast with proper indexing 7. **Open Source**: No licensing costs #### JSONB Usage - **Custom Fields**: Store user-defined fields without schema changes - **Workflow Configuration**: Store workflow states and transitions - **Event Data**: Store domain events as JSON documents - **Metadata**: Store flexible metadata on entities --- ## 4. Frontend Architecture (Next.js 15) ### 4.1 Project Structure ``` colaflow-web/ ├── app/ # Next.js 15 App Router │ ├── (auth)/ # Auth route group │ │ ├── login/ │ │ │ └── page.tsx │ │ ├── register/ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (dashboard)/ # Dashboard route group │ │ ├── projects/ │ │ │ ├── page.tsx # Project list │ │ │ ├── [id]/ │ │ │ │ ├── page.tsx # Project detail │ │ │ │ ├── board/ # Kanban board │ │ │ │ │ └── page.tsx │ │ │ │ ├── tasks/ │ │ │ │ │ ├── page.tsx # Task list │ │ │ │ │ └── [taskId]/ │ │ │ │ │ └── page.tsx # Task detail │ │ │ │ ├── workflows/ │ │ │ │ │ └── page.tsx │ │ │ │ └── audit/ │ │ │ │ └── page.tsx # Audit logs │ │ │ └── create/ │ │ │ └── page.tsx │ │ ├── dashboard/ │ │ │ └── page.tsx # Main dashboard │ │ └── layout.tsx # Dashboard layout │ ├── api/ # API routes (BFF pattern) │ │ └── auth/ │ │ └── [...nextauth]/ │ │ └── route.ts │ ├── layout.tsx # Root layout │ └── page.tsx # Home page │ ├── components/ # Shared components │ ├── ui/ # shadcn/ui components │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ └── ... │ ├── layouts/ │ │ ├── Header.tsx │ │ ├── Sidebar.tsx │ │ └── Footer.tsx │ ├── features/ # Feature-specific components │ │ ├── projects/ │ │ │ ├── ProjectCard.tsx │ │ │ ├── ProjectForm.tsx │ │ │ └── ProjectList.tsx │ │ ├── kanban/ │ │ │ ├── KanbanBoard.tsx │ │ │ ├── KanbanColumn.tsx │ │ │ └── TaskCard.tsx │ │ └── audit/ │ │ ├── AuditLogList.tsx │ │ └── AuditLogDetail.tsx │ └── common/ │ ├── Loading.tsx │ ├── ErrorBoundary.tsx │ └── NotFound.tsx │ ├── lib/ # Utilities and configurations │ ├── api/ # API client │ │ ├── client.ts # Axios instance │ │ ├── projects.ts # Project API calls │ │ ├── tasks.ts # Task API calls │ │ └── users.ts # User API calls │ ├── hooks/ # Custom React hooks │ │ ├── useProjects.ts │ │ ├── useTasks.ts │ │ └── useAuth.ts │ ├── store/ # Zustand stores │ │ ├── authStore.ts │ │ ├── uiStore.ts │ │ └── notificationStore.ts │ ├── utils/ │ │ ├── cn.ts # Tailwind merge utility │ │ ├── format.ts # Formatting utilities │ │ └── validation.ts # Form validation │ └── signalr/ │ └── hubConnection.ts # SignalR connection setup │ ├── types/ # TypeScript types │ ├── api.ts # API response types │ ├── models.ts # Domain models │ └── components.ts # Component prop types │ ├── styles/ │ └── globals.css # Global styles with Tailwind │ ├── public/ │ ├── images/ │ └── icons/ │ ├── .env.local # Environment variables ├── next.config.js ├── tailwind.config.ts ├── tsconfig.json └── package.json ``` ### 4.2 State Management Strategy #### 4.2.1 TanStack Query (Server State) ```typescript // lib/hooks/useProjects.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { projectsApi } from '@/lib/api/projects'; import type { Project, CreateProjectDto } from '@/types/models'; export function useProjects() { return useQuery({ queryKey: ['projects'], queryFn: projectsApi.getAll, staleTime: 5 * 60 * 1000, // 5 minutes }); } export function useProject(id: string) { return useQuery({ queryKey: ['projects', id], queryFn: () => projectsApi.getById(id), enabled: !!id, }); } export function useCreateProject() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (data: CreateProjectDto) => projectsApi.create(data), onSuccess: (newProject) => { // Invalidate and refetch projects list queryClient.invalidateQueries({ queryKey: ['projects'] }); // Optimistically update cache queryClient.setQueryData(['projects'], (old) => old ? [...old, newProject] : [newProject] ); }, }); } export function useUpdateProject(id: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: (data: Partial) => projectsApi.update(id, data), onMutate: async (updatedData) => { // Optimistic update await queryClient.cancelQueries({ queryKey: ['projects', id] }); const previousProject = queryClient.getQueryData(['projects', id]); queryClient.setQueryData(['projects', id], (old) => ({ ...old!, ...updatedData, })); return { previousProject }; }, onError: (err, variables, context) => { // Rollback on error if (context?.previousProject) { queryClient.setQueryData(['projects', id], context.previousProject); } }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['projects', id] }); }, }); } ``` #### 4.2.2 Zustand (Client/UI State) ```typescript // lib/store/uiStore.ts import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; interface UIState { // Sidebar sidebarOpen: boolean; toggleSidebar: () => void; setSidebarOpen: (open: boolean) => void; // Theme theme: 'light' | 'dark' | 'system'; setTheme: (theme: 'light' | 'dark' | 'system') => void; // Modals openModal: string | null; setOpenModal: (modal: string | null) => void; // Notifications notifications: Notification[]; addNotification: (notification: Omit) => void; removeNotification: (id: string) => void; } export const useUIStore = create()( devtools( persist( (set) => ({ sidebarOpen: true, toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })), setSidebarOpen: (open) => set({ sidebarOpen: open }), theme: 'system', setTheme: (theme) => set({ theme }), openModal: null, setOpenModal: (modal) => set({ openModal: modal }), notifications: [], addNotification: (notification) => set((state) => ({ notifications: [ ...state.notifications, { ...notification, id: crypto.randomUUID() }, ], })), removeNotification: (id) => set((state) => ({ notifications: state.notifications.filter((n) => n.id !== id), })), }), { name: 'colaflow-ui-storage', partialize: (state) => ({ sidebarOpen: state.sidebarOpen, theme: state.theme }), } ) ) ); ``` #### 4.2.3 SignalR Real-time Updates ```typescript // lib/signalr/hubConnection.ts import * as signalR from '@microsoft/signalr'; class SignalRService { private connection: signalR.HubConnection | null = null; async connect(token: string) { this.connection = new signalR.HubConnectionBuilder() .withUrl(`${process.env.NEXT_PUBLIC_API_URL}/hubs/project`, { accessTokenFactory: () => token, }) .withAutomaticReconnect() .build(); await this.connection.start(); console.log('SignalR Connected'); } onTaskUpdated(callback: (task: Task) => void) { this.connection?.on('TaskUpdated', callback); } onTaskCreated(callback: (task: Task) => void) { this.connection?.on('TaskCreated', callback); } offTaskUpdated() { this.connection?.off('TaskUpdated'); } async disconnect() { await this.connection?.stop(); } } export const signalRService = new SignalRService(); // Usage in a component import { useEffect } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { signalRService } from '@/lib/signalr/hubConnection'; export function useRealtimeUpdates(projectId: string) { const queryClient = useQueryClient(); useEffect(() => { signalRService.onTaskUpdated((task) => { // Update cache with new task data queryClient.setQueryData(['tasks', task.id], task); queryClient.invalidateQueries({ queryKey: ['projects', projectId, 'kanban'] }); }); return () => { signalRService.offTaskUpdated(); }; }, [projectId, queryClient]); } ``` ### 4.3 Key Components #### 4.3.1 Kanban Board Component ```typescript // components/features/kanban/KanbanBoard.tsx 'use client'; import { DndContext, DragEndEvent, DragOverlay, DragStartEvent } from '@dnd-kit/core'; import { useState } from 'react'; import { KanbanColumn } from './KanbanColumn'; import { TaskCard } from './TaskCard'; import { useKanbanBoard, useUpdateTaskStatus } from '@/lib/hooks/useKanban'; import { useRealtimeUpdates } from '@/lib/hooks/useRealtimeUpdates'; import type { Task } from '@/types/models'; interface KanbanBoardProps { projectId: string; } export function KanbanBoard({ projectId }: KanbanBoardProps) { const { data: kanban, isLoading } = useKanbanBoard(projectId); const updateStatus = useUpdateTaskStatus(); const [activeTask, setActiveTask] = useState(null); // Real-time updates useRealtimeUpdates(projectId); const handleDragStart = (event: DragStartEvent) => { const task = event.active.data.current as Task; setActiveTask(task); }; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over) return; const taskId = active.id as string; const newStatus = over.id as string; // Optimistic update updateStatus.mutate({ taskId, newStatus }); setActiveTask(null); }; if (isLoading) return
Loading...
; return (
{kanban?.columns.map((column) => ( ))}
{activeTask ? : null}
); } ``` --- ## 5. API Design (REST + OpenAPI) ### 5.1 RESTful API Endpoints ``` # Projects GET /api/v1/projects # List all projects POST /api/v1/projects # Create project GET /api/v1/projects/{id} # Get project by ID PUT /api/v1/projects/{id} # Update project DELETE /api/v1/projects/{id} # Delete project GET /api/v1/projects/{id}/kanban # Get Kanban board # Epics GET /api/v1/projects/{projectId}/epics # List epics POST /api/v1/projects/{projectId}/epics # Create epic GET /api/v1/projects/{projectId}/epics/{id} # Get epic PUT /api/v1/projects/{projectId}/epics/{id} # Update epic DELETE /api/v1/projects/{projectId}/epics/{id} # Delete epic # Stories GET /api/v1/epics/{epicId}/stories # List stories POST /api/v1/epics/{epicId}/stories # Create story GET /api/v1/epics/{epicId}/stories/{id} # Get story PUT /api/v1/epics/{epicId}/stories/{id} # Update story DELETE /api/v1/epics/{epicId}/stories/{id} # Delete story # Tasks GET /api/v1/stories/{storyId}/tasks # List tasks POST /api/v1/stories/{storyId}/tasks # Create task GET /api/v1/tasks/{id} # Get task by ID PUT /api/v1/tasks/{id} # Update task PATCH /api/v1/tasks/{id}/status # Update task status DELETE /api/v1/tasks/{id} # Delete task POST /api/v1/tasks/{id}/assign # Assign task to user # Workflows GET /api/v1/projects/{projectId}/workflows # List workflows POST /api/v1/projects/{projectId}/workflows # Create workflow GET /api/v1/workflows/{id} # Get workflow PUT /api/v1/workflows/{id} # Update workflow DELETE /api/v1/workflows/{id} # Delete workflow # Audit Logs GET /api/v1/audit-logs # List all audit logs GET /api/v1/audit-logs/{entityType}/{entityId} # Get entity audit logs POST /api/v1/audit-logs/{id}/rollback # Rollback changes # Users GET /api/v1/users # List users GET /api/v1/users/{id} # Get user POST /api/v1/users # Create user (admin) PUT /api/v1/users/{id} # Update user # Authentication POST /api/v1/auth/login # Login POST /api/v1/auth/register # Register POST /api/v1/auth/refresh # Refresh token POST /api/v1/auth/logout # Logout ``` ### 5.2 Controller Example ```csharp namespace ColaFlow.API.Controllers { [ApiController] [Route("api/v1/[controller]")] [Authorize] public class ProjectsController : ControllerBase { private readonly IMediator _mediator; public ProjectsController(IMediator mediator) { _mediator = mediator; } /// /// Get all projects /// [HttpGet] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task GetProjects( [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken cancellationToken = default) { var query = new GetProjectsQuery(page, pageSize); var result = await _mediator.Send(query, cancellationToken); return Ok(result); } /// /// Get project by ID /// [HttpGet("{id:guid}")] [ProducesResponseType(typeof(ProjectDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetProject( Guid id, CancellationToken cancellationToken = default) { var query = new GetProjectByIdQuery(id); var result = await _mediator.Send(query, cancellationToken); return Ok(result); } /// /// Create a new project /// [HttpPost] [ProducesResponseType(typeof(ProjectDto), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] public async Task CreateProject( [FromBody] CreateProjectCommand command, CancellationToken cancellationToken = default) { var result = await _mediator.Send(command, cancellationToken); return CreatedAtAction(nameof(GetProject), new { id = result.Id }, result); } /// /// Update project /// [HttpPut("{id:guid}")] [ProducesResponseType(typeof(ProjectDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task UpdateProject( Guid id, [FromBody] UpdateProjectCommand command, CancellationToken cancellationToken = default) { command = command with { Id = id }; var result = await _mediator.Send(command, cancellationToken); return Ok(result); } /// /// Delete project /// [HttpDelete("{id:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteProject( Guid id, CancellationToken cancellationToken = default) { var command = new DeleteProjectCommand(id); await _mediator.Send(command, cancellationToken); return NoContent(); } /// /// Get Kanban board for project /// [HttpGet("{id:guid}/kanban")] [ProducesResponseType(typeof(KanbanBoardDto), StatusCodes.Status200OK)] public async Task GetKanbanBoard( Guid id, CancellationToken cancellationToken = default) { var query = new GetProjectKanbanQuery(id); var result = await _mediator.Send(query, cancellationToken); return Ok(result); } } } ``` ### 5.3 OpenAPI Configuration ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // Add OpenAPI builder.Services.AddOpenApi(options => { options.AddDocumentTransformer((document, context, cancellationToken) => { document.Info = new() { Title = "ColaFlow API", Version = "v1", Description = "AI-powered project management system API", Contact = new() { Name = "ColaFlow Team", Email = "api@colaflow.com" } }; return Task.CompletedTask; }); }); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.MapScalarApiReference(); // Modern Swagger UI alternative } ``` --- ## 6. Security Architecture ### 6.1 Authentication & Authorization #### JWT Token-based Authentication ```csharp // Infrastructure/Identity/JwtTokenService.cs public class JwtTokenService : ITokenService { private readonly JwtSettings _jwtSettings; public string GenerateAccessToken(User user) { var claims = new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Email, user.Email), new Claim(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"), new Claim(ClaimTypes.Role, user.Role.ToString()) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey)); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _jwtSettings.Issuer, audience: _jwtSettings.Audience, claims: claims, expires: DateTime.UtcNow.AddHours(_jwtSettings.ExpirationHours), signingCredentials: credentials ); return new JwtSecurityTokenHandler().WriteToken(token); } } ``` #### Authorization Policies ```csharp // Program.cs builder.Services.AddAuthorization(options => { // Role-based policies options.AddPolicy("Admin", policy => policy.RequireRole("Admin")); options.AddPolicy("ProjectManager", policy => policy.RequireRole("Admin", "ProjectManager")); // Resource-based policies options.AddPolicy("ProjectOwner", policy => policy.Requirements.Add(new ProjectOwnerRequirement())); options.AddPolicy("TaskAssignee", policy => policy.Requirements.Add(new TaskAssigneeRequirement())); }); ``` ### 6.2 Data Security - **Encryption at Rest**: PostgreSQL encryption - **Encryption in Transit**: HTTPS/TLS 1.3 - **Password Hashing**: BCrypt with salt - **SQL Injection Protection**: Parameterized queries (EF Core) - **XSS Protection**: Input sanitization, CSP headers - **CSRF Protection**: Anti-forgery tokens --- ## 7. Deployment Architecture ### 7.1 Containerization (Docker) ```dockerfile # Dockerfile for Backend FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src COPY ["src/ColaFlow.API/ColaFlow.API.csproj", "ColaFlow.API/"] COPY ["src/ColaFlow.Application/ColaFlow.Application.csproj", "ColaFlow.Application/"] COPY ["src/ColaFlow.Domain/ColaFlow.Domain.csproj", "ColaFlow.Domain/"] COPY ["src/ColaFlow.Infrastructure/ColaFlow.Infrastructure.csproj", "ColaFlow.Infrastructure/"] RUN dotnet restore "ColaFlow.API/ColaFlow.API.csproj" COPY src/ . WORKDIR "/src/ColaFlow.API" RUN dotnet build "ColaFlow.API.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "ColaFlow.API.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "ColaFlow.API.dll"] ``` ```dockerfile # Dockerfile for Frontend FROM node:20-alpine AS base FROM base AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build FROM base AS runner WORKDIR /app ENV NODE_ENV production COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static EXPOSE 3000 ENV PORT 3000 CMD ["node", "server.js"] ``` ### 7.2 Docker Compose (Development) ```yaml # docker-compose.yml version: '3.8' services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: colaflow POSTGRES_USER: colaflow POSTGRES_PASSWORD: colaflow_dev_password ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U colaflow"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 5 backend: build: context: . dockerfile: Dockerfile.backend ports: - "5000:80" environment: ASPNETCORE_ENVIRONMENT: Development ConnectionStrings__DefaultConnection: Host=postgres;Database=colaflow;Username=colaflow;Password=colaflow_dev_password ConnectionStrings__Redis: redis:6379 JwtSettings__SecretKey: your-secret-key-here-min-32-chars depends_on: postgres: condition: service_healthy redis: condition: service_healthy frontend: build: context: ./colaflow-web dockerfile: Dockerfile ports: - "3000:3000" environment: NEXT_PUBLIC_API_URL: http://backend:80 depends_on: - backend volumes: postgres_data: redis_data: ``` --- ## 8. Testing Strategy ### 8.1 Test Pyramid ``` Unit Tests (80%): - Domain logic (aggregates, entities, value objects) - Application services (commands, queries, handlers) - Utilities and helpers Integration Tests (15%): - API endpoints - Database operations - External service integrations End-to-End Tests (5%): - Critical user flows - Kanban drag-and-drop - Authentication flows ``` ### 8.2 Example Tests ```csharp // Domain.Tests/Aggregates/ProjectTests.cs public class ProjectTests { [Fact] public void Create_ValidData_ShouldCreateProject() { // Arrange var name = "Test Project"; var description = "Test Description"; var key = "TEST"; var ownerId = UserId.Create(Guid.NewGuid()); // Act var project = Project.Create(name, description, key, ownerId); // Assert project.Should().NotBeNull(); project.Name.Should().Be(name); project.Key.Value.Should().Be(key); project.Status.Should().Be(ProjectStatus.Active); project.DomainEvents.Should().ContainSingle(e => e is ProjectCreatedEvent); } [Fact] public void Create_EmptyName_ShouldThrowException() { // Arrange var name = ""; var key = "TEST"; var ownerId = UserId.Create(Guid.NewGuid()); // Act Action act = () => Project.Create(name, "", key, ownerId); // Assert act.Should().Throw() .WithMessage("Project name cannot be empty"); } } ``` --- ## 9. Performance Considerations ### 9.1 Caching Strategy 1. **Redis Caching**: - User sessions (30 min) - Project metadata (5 min) - Kanban board data (2 min) 2. **EF Core Query Optimization**: - `.AsNoTracking()` for read-only queries - Explicit loading with `.Include()` - Pagination for large datasets 3. **SignalR Backplane**: - Redis for scaling SignalR across multiple servers ### 9.2 Database Indexing - Index all foreign keys - Index frequently queried columns (Status, CreatedAt, AssigneeId) - JSONB GIN indexes for custom field queries --- ## 10. Risk Mitigation | Risk | Impact | Mitigation | |------|--------|-----------| | **Complex DDD learning curve** | High | Use established templates (Ardalis, EquinoxProject), pair programming | | **EF Core performance issues** | Medium | Use Dapper for complex queries, implement caching | | **PostgreSQL migration challenges** | Medium | Careful schema design, automated migration testing | | **Real-time performance (SignalR)** | Medium | Redis backplane, connection pooling, load testing | | **Frontend state management complexity** | Low | Clear separation (TanStack Query + Zustand), documentation | --- ## 11. Success Criteria ### M1 Completion Criteria: ✅ Complete project hierarchy (Project → Epic → Story → Task → Subtask) ✅ Functional Kanban board with drag-and-drop ✅ Workflow system with customization ✅ Audit log and rollback capability ✅ All M1 stories complete and tested (80%+ coverage) ✅ API documented with OpenAPI ✅ Frontend responsive and accessible ✅ Real-time updates via SignalR ✅ Docker Compose development environment ✅ CI/CD pipeline ready --- ## 12. Next Steps ### Immediate Actions: 1. **Setup Development Environment**: - Initialize .NET 9 solution with Clean Architecture template - Setup PostgreSQL and Redis via Docker Compose - Initialize Next.js 15 project with App Router - Configure shadcn/ui and Tailwind CSS 2. **Sprint 1 Planning**: - Review this architecture document with team - Create initial database schema - Setup CI/CD pipeline (GitHub Actions) - Begin Project aggregate implementation 3. **Technical Proof of Concepts**: - DDD + CQRS + Event Sourcing with MediatR - EF Core with PostgreSQL JSONB - SignalR real-time updates - Next.js + TanStack Query + Zustand integration --- ## Appendix A: Technology Versions | Technology | Version | Notes | |------------|---------|-------| | .NET | 9.0 | Released November 2024 | | C# | 13.0 | With .NET 9 | | Entity Framework Core | 9.0 | Native AOT support | | PostgreSQL | 16+ | Latest stable | | Redis | 7+ | Latest stable | | Node.js | 20 LTS | For Next.js | | React | 19 | Latest | | Next.js | 15 | App Router | | TypeScript | 5.x | Latest | | Tailwind CSS | 3.x | Latest | | shadcn/ui | Latest | Component library | --- ## Appendix B: References - [Microsoft DDD Microservices](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/) - [Clean Architecture by Uncle Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - [Ardalis CleanArchitecture Template](https://github.com/ardalis/CleanArchitecture) - [Next.js 15 Documentation](https://nextjs.org/docs) - [PostgreSQL JSONB Documentation](https://www.postgresql.org/docs/current/datatype-json.html) - [SignalR Documentation](https://learn.microsoft.com/en-us/aspnet/core/signalr/) --- **Document Status:** ✅ Complete - Ready for Implementation **Next Review:** After Sprint 1 completion **Owner:** Architecture Team **Last Updated:** 2025-11-02