This document describes the technical architecture, data models, and workflows of the Niko Niko Calendar application.
The application follows a distributed architecture composed of several decoupled services orchestrated using Docker.
graph TD
User([User's Browser])
subgraph "Docker Environment"
Frontend[Frontend - React/Vite]
API[Main API - .NET 10]
Notifications[Notification Service - SignalR]
DB[(PostgreSQL/SQLite)]
end
User -->|HTTP/HTTPS| Frontend
User -->|REST API| API
User -->|WebSocket| Notifications
API -->|SQL| DB
API -->|HTTP| Notifications
- Frontend: A React 19 application built with TypeScript and Vite. It provides the user interface for tracking morale and managing teams.
- Main API: A .NET 10 Web API handling business logic, authentication, and data persistence.
- Notification Service: A dedicated .NET service for real-time notifications using SignalR.
- Database: Persistent storage using either PostgreSQL (Production) or SQLite (Development/Portable).
- Framework: React 19 (TypeScript) + Vite
- UI Library: Material UI (MUI v7)
- State Management:
React.Context(Auth, ColorMode),SWR(Data fetching) - Routing:
react-router-dom - Date Handling:
Day.jsfor consistent date formatting and arithmetic.
The application supports multiple languages (English and French) with a centralized management system.
- Library:
i18next/react-i18next. - Configuration:
src/i18n/config.ts. - Storage: Translations are stored in JSON files under
src/i18n/locales/. - Detection:
i18next-browser-languagedetectorautomatically detects user preference. - Date Formatting:
Day.jslocales are dynamically updated based on the selected language.
The backend is architected following the Skinny Controller pattern to ensure maintainability and testability.
- API Layer (NikoNiko.Api): Handles HTTP concerns (routing, input binding, status codes).
- Business Logic Layer (NikoNiko.Services): Contains the concrete implementations of business logic and validations. Key responsibilities include:
- Team Quota: Enforcing the limit of 2 teams per regular user (configurable via
MAX_TEAMS_PER_USER). - Sprint Validation:
- No Overlap: Preventing the creation or update of sprints that overlap in time for the same team.
- Duration Limit: Ensuring a sprint does not exceed 2 months (62 days).
- Mood Entry Validation: Restricting entries to the current sprint range and preventing future dates.
- User Management: Distinguishing between removing a user from a team (soft/team-level) and deleting a user account (global).
- Team Quota: Enforcing the limit of 2 teams per regular user (configurable via
- Core Layer (NikoNiko.Core): Defines DTOs, domain models, and service interfaces.
- Data Layer (NikoNiko.Data): Manages data access via Entity Framework Core, including migrations and database-specific configurations (PostgreSQL/SQLite).
The following diagram illustrates the core data models and their relationships.
erDiagram
USER ||--o{ TEAM_USER : participates
USER ||--o{ TEAM : administers
USER ||--o{ BADGE : earns
USER ||--o{ MOOD_ENTRY : records
USER ||--o{ TEAM_INVITATION : creates
USER ||--o{ REFRESH_TOKEN : owns
TEAM ||--o{ TEAM_USER : contains
TEAM ||--o{ SPRINT : has
TEAM ||--o{ TEAM_INVITATION : issues
SPRINT ||--o{ MOOD_ENTRY : includes
USER {
Guid Id
string OAuthId
string Email
string Name
string AvatarUrl
bool IsSuperAdmin
bool IsOnboarded
DateTime CreatedAt
}
TEAM {
Guid Id
string Name
Guid AdminId
int DefaultSprintDuration
string SprintNameTemplate
DateTime CreatedAt
}
TEAM_USER {
Guid UserId
Guid TeamId
bool IsAdmin
DateTime JoinedAt
}
SPRINT {
Guid Id
string Name
DateTime StartDate
DateTime EndDate
Guid TeamId
}
MOOD_ENTRY {
Guid Id
Guid UserId
Guid SprintId
DateTime Date
string Mood
}
TEAM_INVITATION {
Guid Id
Guid TeamId
string Token
DateTime ExpirationDate
string Status
Guid AcceptedByUserId
}
REFRESH_TOKEN {
Guid Id
string Token
DateTime Expires
DateTime Created
Guid UserId
}
Authentication is handled via OAuth 2.0 (GitHub/Google/Discord) and secured using JWT (JSON Web Tokens).
- OAuth Challenge: User triggers login via the provider of choice.
- Callback: After provider consent, the API exchanges the code for profile info.
- Token Generation: The API generates a short-lived Access Token (JWT) and a secure, HttpOnly Refresh Token.
- Authorization: Subsequent requests include the Bearer token in the
Authorizationheader. - Silent Refresh: When the Access Token expires, the Frontend automatically uses the Refresh Token cookie to obtain a new Access Token.
sequenceDiagram
participant U as User
participant F as Frontend
participant A as Main API
participant P as OAuth Provider (GitHub/Google/Discord)
U->>F: Clicks Login
F->>A: Redirects to /api/auth/login-{provider}
A->>P: Challenge OAuth Request
P->>U: Requests Consent
U->>P: Grants Permission
P->>A: Callback with Code (/signin-{provider})
A->>A: Exchange Code for Profile Info
A->>A: Create/Update User
A->>A: Generate Access Token (JWT) & Refresh Token
A->>F: Redirect with JWT & Set HttpOnly Cookie
F->>F: Store JWT in LocalStorage
F->>A: API Request (Authorization: Bearer <Token>)
opt Token Expiration
F->>A: API Request (401 Unauthorized)
F->>A: Request Refresh (/api/auth/refresh-token) with Cookie
A->>A: Validate Refresh Token
A->>F: New Access Token
F->>A: Retry API Request
end
The application uses SignalR for real-time updates, utilizing SignalR Groups to ensure team-isolated broadcasting.
- Connection: Upon connecting, the user is added to SignalR groups corresponding to their
team_idclaims found in the JWT. - Team Isolation: Notifications (like mood submissions) are only broadcast to the specific team group, ensuring users only see updates relevant to their teams.
- Dynamic Membership: When a user joins or leaves a team, the backend dynamically updates their SignalR group membership.
- Calendar Dates (Sprints, Birthdays): Stored as
DateTimeat Midnight UTC (DateTime.SpecifyKind(date.Date, DateTimeKind.Utc)). - Point-in-Time (Logs, Events, MoodEntries): Stored as True UTC (
DateTime.UtcNow).
- Backend: Adheres to
.editorconfigrules; formatted viadotnet format. - Frontend: Formatted via
Prettierand linted viaESLint. Uses Material UI path imports for bundle optimization. - Planning: All implementation plans are written in Markdown and strictly in English.