Clean-lite, 4 projects in [mws.backend.dotnet.sln](mws.backend.dotnet.sln). Dependencies point inward only: `Api → Application ← Infrastructure`, both `Application` and `Infrastructure → Domain`.
- **[Mws.Domain](Mws.Domain/)** — POCOs only. Entities (`User`, `Project`, `ProjectMember`, `TaskItem`, `Document`, `Role`, `RolePermission`, `UserRole`, `MasterDataEntry`) and enums (`ProjectStatus`, `MemberRole`, `TaskStatus`, `TaskPriority`, `DocumentType`). No EF attributes. Roles are data (`roles` table), not a hardcoded enum — users link to roles through the `UserRole` join table (`user_roles`, many-to-many; `User.UserRoles`).
- **[Mws.Application](Mws.Application/)** — use cases. One folder per module (`Auth/`, `Users/`, `Projects/`, `Tasks/`, `Documents/`, `MasterData/`, `Permissions/`) plus shared `Common/` (DTOs, repositories, exceptions). Module names match controllers/routes (`api/users`, `api/permissions`, `api/masterdata`). Use cases are MediatR command/query handlers; each depends on `IUnitOfWork` (via `Common/Repositories/`) and `IPermissionService`, never on EF Core directly.
- **[Mws.Infrastructure](Mws.Infrastructure/)** — EF Core + auth wiring. `AppDbContext` (in [Mws.Infrastructure/Persistence/AppDbContext.cs](Mws.Infrastructure/Persistence/AppDbContext.cs)) configures all entities via fluent API and stores enums as strings. `Authentication/` contains `BcryptPasswordHasher` and `JwtTokenService`. Migrations live under `Migrations/`. `DependencyInjection.AddInfrastructure` is the single composition root.
- **[Mws.Api](Mws.Api/)** — controllers, middleware, `Program.cs`. Thin: controllers resolve a service via DI, get the user id from `User.GetUserId()` ([Mws.Api/ClaimsExtensions.cs](Mws.Api/ClaimsExtensions.cs)), and delegate.
Exceptions in [Mws.Application/Common/Exceptions.cs](Mws.Application/Common/Exceptions.cs) (`BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`) are caught by [Mws.Api/Middleware/ApiExceptionMiddleware.cs](Mws.Api/Middleware/ApiExceptionMiddleware.cs) and mapped to 400/401/403/404. Anything else → 500 with a generic message (original logged). Use these exceptions in handlers instead of returning `Result<T>`.
1.**Project membership** (unchanged). JWT carries `sub` = user `Guid`. Every protected project/task/document endpoint looks up `ProjectMember` to verify the caller belongs to the project — checks live in the application handlers via `ProjectAccess`/`TaskAccess` helpers and `IProjectRepository` (`GetMemberRoleAsync`). Mutating project settings requires `MemberRole.Owner`. Add a new check the same way.
2.**Screen-level role permissions** (admin screens only: Users, Permissions, Master Data). `Role` is a DB entity (`roles` table), not an enum — a user links to roles via the `UserRole` join table. `RolePermission` is a per-(role, screen) CRUD matrix (`CanView`/`CanCreate`/`CanEdit`/`CanDelete`). `IPermissionService.EnsureAsync(userId, screen, action)` (in `Mws.Application/Permissions/PermissionService.cs`) is the gate — called at the top of the users/roles/masterdata handlers. There is **no**`[Authorize(Roles=...)]` JWT-claim-based gating — the JWT only carries `sub`/`name`; role/permission is always resolved fresh from the DB per request, so changing a role's permissions or a user's roles takes effect immediately without re-login. `ScreenCatalog.cs` is the single source of truth for valid screen keys (`dashboard`, `projects`, `tasks`, `documents`, `users`, `permissions`, `masterdata`) — adding a new admin-gated screen means adding it there. `GET /api/menu` returns the CRUD matrix per screen for the caller's roles so the frontend can render nav + gate buttons; it does not itself enforce anything server-side.
- PostgreSQL via `Npgsql.EntityFrameworkCore.PostgreSQL` 10. Connection string key is `ConnectionStrings:Default` (see [Mws.Api/appsettings.json](Mws.Api/appsettings.json)); fallback to `localhost:5432` dev creds `mws/mws`. Override via env if you must.
- Enum columns are stored as `varchar(20)` (not ints) — preserve that for any new enum.
- Self-referencing `Document.ParentId` uses `Restrict` to block accidental cycles. Cascade delete is implemented in `DeleteDocumentHandler` via a manual descendant walk (`DocumentAccess.DeleteDescendantsAsync`); do not switch the FK to cascade.
-`DbSeeder` ([Mws.Infrastructure/Persistence/DbSeeder.cs](Mws.Infrastructure/Persistence/DbSeeder.cs)) seeds two system roles (`Admin` — full CRUD on every screen; `Member` — view-only on Projects, CRUD on Tasks/Documents (no delete), and no access to Users/Permissions/Master Data) plus admin/namdh (password: `password`, admin → Admin role, namdh → Member) and one demo project on first boot. System roles are re-synced to this canonical permission matrix on every startup (`SyncSystemRoleAsync`), so edits made to them through the Roles UI do not persist across restarts. Skips user/project seeding if any users exist. System roles (`Role.IsSystem = true`) can't be deleted via `DeleteRoleCommand`.
- All async service methods take `CancellationToken ct = default` and forward it.
- Enums serialized as strings globally (`JsonStringEnumConverter` in `Program.cs`).
## Frontend
The companion frontend lives at `../project/mws/` (Vite/React). API root in dev: `http://localhost:5xxx` → backend at `http://localhost:5xxx` per CORS config — confirm port mapping if backend URL changes.