A full-stack blog platform featuring real-time interactions, CQRS-inspired architecture, fine-grained authorization, and hybrid authentication.
π Live Demo: https://marko-blog.vercel.app/
- Overview
- Problem & Motivation
- Key Features
- Demo Credentials
- Tech Stack
- Architecture Explained
- Authentication System
- Real-Time Notifications
- Authorization Model
- Database Schema
- Folder Structure
- Setup Instructions
- Environment Variables
- Database Seed
- Deployment
- Tradeoffs & Limitations
Marko's Blog is a full-stack web application that allows users to create, interact with, and follow blog content in real time.
The system is designed to demonstrate:
- Scalable backend architecture (CQRS-inspired with Clean Architecture)
- Real-time communication via SignalR (WebSockets)
- Custom authorization logic beyond basic role checks
- Integration of external authentication (Google via Firebase)
Most blog platforms are simple CRUD applications with minimal real-time capabilities and tightly coupled logic. This project explores how to:
- Separate read and write operations for better structure and maintainability
- Handle real-time user interactions (likes, comments, follows, posts)
- Design a flexible authorization system beyond basic role assignments
- Integrate external identity providers while maintaining full backend control
- π Create, edit, and manage blog posts with categories and images
- π¬ Comment system with nested replies (threaded comments)
- β€οΈ Like/unlike posts and comments
- π₯ Follow system - users receive updates from authors they follow
- π Real-time notifications via WebSockets (SignalR)
- π Hybrid authentication (JWT + Google via Firebase)
- π§ Use-case driven architecture with centralized execution pipeline
- π Full audit log of all executed use-case operations
- π‘ Fine-grained authorization per action (not just role-based)
- π§ Email notification on registration (SMTP)
- π Author request system - users can apply to become authors
- π Light / Dark theme toggle
Use these accounts to explore the live demo:
π¨βπ» Admin login:
Username: admin
Password: admin123
π¨βπ» Author login:
Username: emily_s
Password: pass123
The backend follows a layered architecture inspired by Clean Architecture and CQRS principles.
| Layer | Responsibility |
|---|---|
Domain |
Core business entities and interfaces - no infrastructure dependencies |
EFDataAccess |
Database access via Entity Framework Core, configurations, migrations |
Application |
Use-case interfaces (CQRS), DTOs, validators, service contracts |
Implementation |
Concrete implementations of commands, queries, services, validators |
API |
ASP.NET Core Web API - controllers, middleware, JWT, SignalR, DI config |
Client |
React frontend - routing, Redux state, real-time layer |
- Commands β mutate system state (Create, Update, Delete)
- Queries β read data only
CreatePostCommand β creates a post + triggers notifications
GetPostsQuery β fetches filtered and paginated posts
Every action flows through a single UseCaseExecutor:
HTTP Request
β
UseCaseExecutor
βββ Logs the request (actor, data, use-case name)
βββ Checks permissions (AllowedUseCases)
βββ Executes the use-case
The app uses a hybrid authentication approach.
- User sends
username+password - Backend validates credentials (BCrypt hash check)
- JWT is issued and returned
- User signs in via Google popup (Firebase SDK)
- Frontend receives user data (email, name, avatar)
- Data is sent to
/api/auth - Backend checks if user exists - creates one if needed
- JWT is issued and returned
β οΈ Firebase is used only as an identity provider. JWT is the sole source of authorization. Firebase tokens are not verified on the backend (a production requirement).
The system combines REST and WebSocket (SignalR) communication.
Notifications are fetched via REST API on app start.
User performs action (like / comment / follow / post)
β
Backend creates a Notification record
β
SignalR pushes it to the target user's group
β
Frontend Redux state updates instantly
β
UI re-renders
Each user is assigned to a SignalR group by their IdUser, so notifications are targeted and multi-device friendly.
Instead of classic [Authorize(Roles = "Admin")] on controllers, this system uses use-case level permissions.
- Each user has a list of allowed
UseCaseEnumvalues stored inUserUseCases - Permissions are derived from their role (Admin / Author / User) and automatically updated when role changes
- Every action is validated inside
UseCaseExecutorbefore execution
This enables:
- Fine-grained control (e.g., delete own comment vs. delete any comment)
- Easy extension of permissions without touching controllers
- Full auditability via use-case logs
My_Blog/
βββ Domain/ # Business entities (Post, User, Comment, etc.)
β βββ *.cs
β
βββ EFDataAccess/ # EF Core DbContext, configurations, migrations
β βββ BlogContext.cs
β βββ Configurations/
β βββ Seed/
β
βββ Application/ # CQRS interfaces, DTOs, validators, services
β βββ Commands/
β βββ Queries/
β βββ DataTransfer/
β βββ Searches/
β βββ Services/
β βββ Exceptions/
β
βββ Implementation/ # Concrete implementations
β βββ Commands/
β βββ Queries/
β βββ Services/
β βββ Validators/
β βββ Logging/
β βββ Extensions/
β
βββ API/ # ASP.NET Core Web API
β βββ Controllers/
β β βββ PostsController.cs
β β βββ CommentsController.cs
β β βββ CategoriesController.cs
β β βββ LikesController.cs
β β βββ FollowersController.cs
β β βββ NotificationsController.cs
β β βββ UsersController.cs
β β βββ LoginController.cs
β β βββ OAuthController.cs
β β βββ RegisterController.cs
β β βββ AuthorRequestsController.cs
β β βββ ImagesController.cs
β β βββ UseCaseLogsController.cs
β βββ Core/
β β βββ JWTManager.cs
β β βββ JWTService.cs
β β βββ NotificationHub.cs
β β βββ SignalRNotificationHub.cs
β β βββ GlobalExceptionHandler.cs
β β βββ APIExtension.cs
β βββ Services/
β βββ Startup.cs
β βββ Program.cs
β
βββ Client/ # React frontend
βββ src/
βββ pages/
β βββ Home.jsx
β βββ PostsPage.jsx
β βββ PostPage.jsx
β βββ CreatePost.jsx
β βββ UpdatePost.jsx
β βββ Dashboard.jsx
β βββ SignIn.jsx
β βββ SignUp.jsx
β βββ UserPage.jsx
β βββ Authors.jsx
β βββ CategoryPage.jsx
β βββ CreateCategory.jsx
β βββ NotificationsPage.jsx
β βββ UserCommentPage.jsx
βββ components/
β βββ Header.jsx
β βββ Footer.jsx
β βββ PostCard.jsx
β βββ CommentSection.jsx
β βββ Comment.jsx
β βββ ChildComment.jsx
β βββ AdminDashboard.jsx
β βββ DashPosts.jsx
β βββ DashComments.jsx
β βββ DashUsers.jsx
β βββ DashCategories.jsx
β βββ DashLogs.jsx
β βββ DashAuthorRequests.jsx
β βββ DashProfile.jsx
β βββ PrivateRoute.jsx
β βββ OnlyRolePrivateRoute.jsx
β βββ ThemeProvider.jsx
β βββ OAuth.jsx
βββ redux/
β βββ user/userSlice.js
β βββ theme/themeSlice.js
β βββ notification/notificationsSlice.js
βββ contexts/
β βββ ErrorContext.jsx
β βββ SuccessContext.jsx
βββ api/
βββ services/
βββ utils/
βββ App.jsx
βββ main.jsx
- .NET 8 SDK
- Node.js 18+
- PostgreSQL running locally
- A Firebase project (for Google OAuth)
git clone https://github.com/your-username/marko-blog.git
cd marko-blogInside the API/ folder, update appsettings.Development.json with your local values:
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=blog;Username=postgres;Password=postgres;SSL Mode=Disable"
},
"JWT": {
"Issuer": "http://localhost:5000",
"Audience": "BlogClient",
"SecretKey": "your-dev-secret-key-min-32-chars",
"TokenExpiryMinutes": 120
},
"SMTP": {
"SenderEmail": "noreply@yourdomain.com",
"Host": "smtp.yourdomain.com",
"Port": 587,
"Username": "noreply@yourdomain.com",
"Password": "your-smtp-password"
}
}cd API
# Restore dependencies
dotnet restore
# Apply database migrations (creates tables)
dotnet ef database update
# Start the API
dotnet runThe API will be available at http://localhost:5000.
cd Client
# Install dependencies
npm install
# Start the development server
npm run devThe frontend will be available at http://localhost:5173.
| Key | Description |
|---|---|
ConnectionStrings:DefaultConnection |
PostgreSQL connection string |
JWT:Issuer |
Token issuer (your API URL) |
JWT:Audience |
Token audience (your client name) |
JWT:SecretKey |
Secret key - minimum 32 characters |
JWT:TokenExpiryMinutes |
Token lifetime in minutes |
SMTP:Host |
SMTP server host |
SMTP:Port |
SMTP port (587 for TLS) |
SMTP:SenderEmail |
Email address used for sending |
SMTP:Password |
App password for SMTP auth |
Create a .env file inside the Client/ folder:
VITE_API_URL=http://localhost:5000/api
VITE_FIREBASE_API_KEY=your_key
VITE_FIREBASE_AUTH_DOMAIN=your_domain
VITE_FIREBASE_PROJECT_ID=your_projectπ‘ The Vite dev server proxies
/apirequests tohttp://localhost:5000automatically - you only needVITE_API_URLfor production builds.
The application automatically seeds the database on first run via DataSeeder.cs.
Seeded data includes:
- Roles - Admin, Author, User
- Users - including
adminandemily_saccounts with use-case permissions - Categories - initial blog categories
- Posts - sample blog posts with images
- PostβCategory links
- Comments - sample comments with nested replies
- Followers - sample follow relationships
- Likes - sample likes on posts and comments
- Notifications - sample notification entries
Seeding only runs when the respective tables are empty, so it is safe to re-run.
To manually trigger a fresh seed, drop and recreate the database:
dotnet ef database drop
dotnet ef database update| Layer | Platform |
|---|---|
| Frontend | Vercel |
| Backend | Fly.io |
| Database | PostgreSQL on Fly.io |
The project includes a Dockerfile and fly.toml for backend deployment, and a vercel.json for frontend deployment.
- Firebase token is not verified on the backend (would be required in production)
- No recovery mechanism for missed SignalR events (e.g., notifications sent while offline)
- Commands handle side-effects (like notifications) directly - no event bus or message queue
UseCaseEnummay become harder to maintain at scale- Images are stored locally on the server (not cloud-optimized - no S3 or CDN)
- Two sources of truth for auth state: Redux + localStorage (can desync on edge cases)
This project goes beyond a typical CRUD blog and demonstrates:
- Real-time full-stack application design with SignalR
- Clean separation of concerns across multiple backend layers
- Custom authorization beyond simple role checks
- Integration of external OAuth providers (Google/Firebase)
- Practical application of SOLID principles in a layered architecture
- CQRS approach with centralized use-case execution and audit logging
It represents a step toward production-oriented system design in the .NET + React ecosystem.