Skip to content

System Architecture

High-Level Overview

MedRegia is a modular monolith — a single deployable unit with clearly separated internal modules. The frontend is a Single Page Application (SPA) that communicates exclusively through the REST API.

mermaid
graph TB
    subgraph Browser["Browser (Vue 3 SPA)"]
        FSD["Feature-Sliced Design layers<br/>pages → widgets → features → shared"]
        PERM["usePermission()<br/>JWT claim check"]
    end

    subgraph API["Backend (.NET 10)"]
        MW["JWT Middleware"]
        FILTER["RequirePermission Filter"]
        subgraph MOD["Modules"]
            IDENTITY["Identity<br/>Auth · Users · Roles · RBAC"]
            VENDOR["VendorManagement<br/>Vendors · Invoices · Transactions"]
            SETTINGS["Settings"]
            DASHBOARD["Dashboard"]
            REPORTS["Reports"]
            ONBOARDING["Onboarding"]
        end
        DAPPER["Dapper + IDbConnectionFactory"]
        AUTOMAPPER["AutoMapper"]
    end

    subgraph DB["MySQL 8"]
        TABLES[("Tables")]
        DBUP["DbUp Migrations"]
    end

    FSD -->|"HTTP + Bearer JWT"| MW
    MW --> FILTER
    FILTER --> MOD
    MOD --> DAPPER
    MOD --> AUTOMAPPER
    DAPPER --> TABLES
    DBUP -.->|"schema versioning"| TABLES

Request Lifecycle

mermaid
sequenceDiagram
    participant Vue as Vue SPA
    participant Nginx as Nginx
    participant API as ASP.NET Core
    participant Filter as PermissionFilter
    participant Svc as Service
    participant DB as MySQL

    Vue->>Nginx: GET /api/vendors (Bearer JWT)
    Nginx->>API: proxy_pass /api/vendors
    API->>Filter: [RequirePermission("Vendor","Read")]
    Filter->>Filter: Decode JWT claims
    alt is_admin = "true"
        Filter-->>API: Bypass all checks
    else
        Filter->>Filter: Check "Vendor:Read" in perm[] claims
        alt Missing permission
            Filter-->>Vue: 403 Forbidden (ProblemDetails)
        end
    end
    API->>Svc: GetVendorsPagedAsync(...)
    Svc->>DB: SELECT ... FROM vendors
    DB-->>Svc: rows
    Svc-->>API: (Items, TotalCount)
    API-->>Vue: 200 { items, totalCount, page, pageSize }

Module Structure

Each module follows the same four-layer layout:

Modules/[ModuleName]/
├── Domain/          ← POCO models (no annotations, no logic)
│   ├── [Entity].cs
│   └── Enums/
├── Data/            ← Dapper repositories (SQL only)
│   ├── Interfaces/
│   └── Repositories/
├── Business/        ← All business logic
│   ├── DTOs/
│   ├── Interfaces/
│   ├── Profiles/    ← AutoMapper
│   └── Services/
└── Controller/      ← HTTP interface (maps DTOs, calls services)

Data Serialisation Pipeline

MySQL (snake_case)
    ↓  Dapper  MatchNamesWithUnderscores
C# Domain (PascalCase)
    ↓  AutoMapper
DTO (PascalCase)
    ↓  System.Text.Json  CamelCase policy
JSON (camelCase)
    ↓  TypeScript interface
Frontend (camelCase)

Example: vendor_idVendorId (C#) → vendorId (JSON/TS)

Authentication Flow

mermaid
sequenceDiagram
    participant U as User (Browser)
    participant API as POST /api/auth/login
    participant Auth as AuthService
    participant DB as MySQL

    U->>API: { email, password }
    API->>Auth: LoginAsync(email, password)
    Auth->>DB: SELECT user WHERE email = ?
    DB-->>Auth: user row
    Auth->>Auth: BCrypt.Verify(password, hash)
    Auth->>DB: SELECT permissions for user
    DB-->>Auth: permission rows
    Auth->>Auth: Build JWT with perm[] claims
    Auth-->>API: { token, expiresAt, user }
    API-->>U: 200 LoginResponseDto
    U->>U: Store token in localStorage
    U->>U: All subsequent requests: Authorization: Bearer <token>

RBAC Model

mermaid
graph LR
    USER["User"] -->|"has one"| ROLE["Role"]
    ROLE -->|"linked to many"| POLICY["Policy"]
    USER -->|"also linked directly"| POLICY
    POLICY -->|"contains many"| PERM["Permission<br/>(Collection:Action)"]

    style PERM fill:#16a34a,color:#fff
    style POLICY fill:#d97706,color:#fff
    style ROLE fill:#7c3aed,color:#fff
    style USER fill:#2563eb,color:#fff

Permissions are merged from both the user's role policies and any user-direct policies. Admin users receive is_admin: "true" in their JWT which bypasses all permission checks.

Modules at a Glance

ModuleRoute PrefixResponsibility
Identity/api/auth, /api/users, /api/roles, /api/permissions, /api/policiesAuthentication, users, RBAC
VendorManagement/api/vendors, /api/invoices, /api/transactionsCore business domain
Settings/api/settingsApp-wide key-value config
Dashboard/api/dashboardRead-only aggregate queries
Reports/api/reportsExcel export endpoints
Onboarding/api/onboardingFirst-run setup (no auth required)
UserProfile/api/profileAuthenticated user's own profile

MedRegia — Pharmaceutical Vendor Management Platform