NNews is a multi-tenant CMS (Content Management System) microservice for news and blogs with AI-powered content generation via ChatGPT and DALL-E 3. Built using .NET 8, PostgreSQL 16, and Clean Architecture, it provides a complete REST API for managing articles, categories, tags, and images β with full tenant isolation through separate databases and per-tenant JWT authentication.
NNews is part of the Emagine ecosystem and integrates with NAuth for authentication/user management and zTools for AI content generation, file uploads, and utility services.
- π’ Multi-Tenant Architecture - Complete tenant isolation with separate databases and per-tenant JWT secrets
- π€ AI Content Generation - Create and update articles using ChatGPT integration
- πΌοΈ DALL-E 3 Image Generation - AI-powered image creation for articles
- π° Full CMS - CRUD for articles, categories (hierarchical), and tags (with merge support)
- π Per-Tenant JWT Authentication - Dynamic JWT validation with tenant-specific signing keys
- π File Upload - Image upload to S3 via zTools integration
- π·οΈ Role-Based Access - Article-level role-based access control
- π Article Status Workflow - Draft, Published, Archived, and Scheduled states
- π¦ NuGet Packages - ACL and DTO published as NuGet packages for external consumption
- π Structured Logging - Serilog with console and file sinks
- π³ Docker Ready - Full Docker Compose setup for development and production
- ASP.NET Core 8.0 - Web API framework
- Entity Framework Core 8.0 - ORM with PostgreSQL provider (Npgsql)
- PostgreSQL 16 - Primary database (one per tenant)
- NAuth (v0.5.5) - Authentication, user management, and JWT with multi-tenant support
- Microsoft.IdentityModel.Tokens - Per-tenant JWT validation via
IssuerSigningKeyResolver
- zTools (v0.3.6) - ChatGPT, DALL-E, file upload (S3), slug generation, email
- AutoMapper - Entity-to-DTO mapping
- Serilog - Structured logging (Console + File sinks)
- Swashbuckle - Swagger/OpenAPI documentation
- Newtonsoft.Json - JSON serialization
- Docker & Docker Compose - Containerized deployment
- GitHub Actions - CI/CD (versioning, NuGet publishing, production deploy)
- GitVersion - Semantic versioning (ContinuousDelivery mode)
NNews/
βββ NNews.API/ # ASP.NET Core Web API
β βββ Controllers/ # REST controllers (Article, Category, Tag, Image)
β βββ Middlewares/ # TenantMiddleware (multi-tenant resolution)
β βββ Program.cs # App startup & pipeline configuration
βββ NNews.Application/ # DI registration & initialization
β βββ Interfaces/ # ITenantContext, ITenantDbContextFactory
β βββ Services/ # TenantContext, TenantDbContextFactory, NAuthProviders
β βββ Initializer.cs # Service registration entry point
βββ NNews.Domain/ # Business logic & entities
β βββ Entities/ # Domain models (Article, Category, Tag, ArticleRole)
β βββ Enums/ # ArticleStatus, etc.
β βββ Services/ # Domain services & interfaces
βββ NNews.Infra/ # Infrastructure layer
β βββ Context/ # EF Core DbContext (NNewsContext)
β βββ Mapping/ # AutoMapper profiles
β βββ Migrations/ # EF Core migrations
β βββ Repository/ # Repository implementations
βββ NNews.Infra.Interfaces/ # Repository interface contracts
βββ NNews/ (NuGet packages)
β βββ NNews.ACL/ # Anti-Corruption Layer HTTP clients
β βββ Handlers/ # TenantHeaderHandler (auto-injects X-Tenant-Id)
β βββ Interfaces/ # ACL client interfaces + ITenantResolver
β βββ Services/ # TenantResolver
βββ docs/ # Project documentation
βββ .github/workflows/ # CI/CD pipelines
βββ docker-compose.yml # Development environment
βββ docker-compose-prod.yml # Production environment (multi-tenant)
βββ NNews.API.Dockerfile # Multi-stage Docker build
βββ postgres.Dockerfile # PostgreSQL with extensions
βββ nnews.sql # Database schema
βββ README.md # This file
| Project | Type | Package | Description |
|---|---|---|---|
| NNews | Microservice | CMS API (ACL + DTO) | |
| NAuth | Microservice | Authentication & user management | |
| zTools | Microservice | ChatGPT, DALL-E, file upload, utilities |
nnews-app (React SPA)
βββ NNews.ACL (NuGet) βββ NNews API βββ PostgreSQL (per tenant)
βββ nauth-react β
ββββ NAuth API (auth + users)
ββββ zTools API (ChatGPT, S3, utils)
The following diagram illustrates the high-level architecture of NNews:
The NNews API receives requests with tenant identification via X-Tenant-Id header (for unauthenticated endpoints) or JWT tenant_id claim (for authenticated endpoints). The TenantMiddleware resolves the tenant before authentication, and the TenantDbContextFactory dynamically connects to the correct tenant database. Each tenant has its own PostgreSQL database and JWT signing secret.
π Source: The editable Mermaid source is available at
docs/system-design.mmd.
NNews implements the database-per-tenant isolation pattern:
| Scenario | TenantId Source |
|---|---|
| Unauthenticated endpoints | X-Tenant-Id HTTP header |
| Authenticated endpoints | tenant_id JWT claim |
| ACL consumers (NuGet package) | appsettings.json β Tenant:DefaultTenantId (auto-injected via TenantHeaderHandler) |
| Component | Layer | Responsibility |
|---|---|---|
TenantMiddleware |
API | Extracts X-Tenant-Id from header before authentication |
TenantContext |
Application | Resolves TenantId from JWT claim or HTTP header (scoped) |
TenantResolver |
ACL | Reads tenant config (ConnectionString, JwtSecret) from appsettings |
TenantDbContextFactory |
Application | Creates NNewsContext with dynamic ConnectionString per tenant |
TenantHeaderHandler |
ACL | DelegatingHandler that auto-injects X-Tenant-Id in all ACL HTTP requests |
{
"Tenant": {
"DefaultTenantId": "emagine"
},
"Tenants": {
"emagine": {
"ConnectionString": "Host=db;Database=nnews_emagine;...",
"JwtSecret": "your_64char_secret_for_emagine"
},
"devblog": {
"ConnectionString": "Host=db;Database=nnews_devblog;...",
"JwtSecret": "your_64char_secret_for_devblog"
}
}
}- TenantId is never accepted from request body β only from header or JWT
- Each tenant has its own JwtSecret β JWT validation uses
IssuerSigningKeyResolverto dynamically resolve the signing key - ACL consumers never pass TenantId as a method parameter β it's propagated automatically via
TenantHeaderHandler
| Document | Description |
|---|---|
| MULTI_TENANT_API | Multi-tenant implementation guide and patterns |
| USER_API_DOCUMENTATION | User API endpoints reference |
| ROLE_API_DOCUMENTATION | Role API endpoints reference |
| NUGET_PUBLISHING_GUIDE | Guide for publishing NuGet packages |
cp .env.example .envEdit the .env file:
# PostgreSQL Container
POSTGRES_DB=nnews_db
POSTGRES_USER=nnews_user
POSTGRES_PASSWORD=your_secure_password_here
POSTGRES_PORT=5433
# NNews API Port
API_HTTP_PORT=5007
# External Services
ZTOOL_API_URL=http://ztools-api:8080cp .env.prod.example .env.prodEdit the .env.prod file:
# HTTPS Certificate
CERTIFICATE_PASSWORD=your_certificate_password_here
# Tenant: emagine
EMAGINE_CONNECTION_STRING=Host=your_db_host;Port=5432;Database=nnews_emagine_db;Username=your_user;Password=your_password
EMAGINE_JWT_SECRET=your_emagine_jwt_secret_at_least_64_characters_long
# Tenant: devblog
DEVBLOG_CONNECTION_STRING=Host=your_db_host;Port=5432;Database=nnews_devblog_db;Username=your_user;Password=your_password
DEVBLOG_JWT_SECRET=your_devblog_jwt_secret_at_least_64_characters_long- Never commit
.envor.env.prodfiles with real credentials - Only
.env.exampleand.env.prod.exampleshould be version controlled - Change all default passwords and secrets before deployment
# Create the shared Docker network
docker network create emagine-networkdocker-compose up -d --builddocker-compose ps
docker-compose logs -fdocker compose --env-file .env.prod -f docker-compose-prod.yml up --build -d| Service | URL | Environment |
|---|---|---|
| NNews API (HTTP) | http://localhost:5007 | Dev / Prod |
| NNews API (HTTPS) | https://localhost:5008 | Prod only |
| PostgreSQL | localhost:5433 | Dev only |
| Swagger UI | http://localhost:5007/swagger | Dev only |
| Action | Command |
|---|---|
| Start services | docker-compose up -d |
| Start with rebuild | docker-compose up -d --build |
| Stop services | docker-compose stop |
| View status | docker-compose ps |
| View logs | docker-compose logs -f |
| Remove containers | docker-compose down |
| Remove containers and volumes ( |
docker-compose down -v |
- .NET 8 SDK
- PostgreSQL 16
- NAuth API running (for authentication)
- zTools API running (for AI features)
git clone https://github.com/emaginebr/NNews.git
cd NNews
dotnet restore NNews.slnCreate a PostgreSQL database and update appsettings.Development.json:
{
"ConnectionStrings": {
"NNewsContext": "Host=localhost;Port=5432;Database=nnews_db;Username=your_user;Password=your_password"
}
}psql -U your_user -d nnews_db -f nnews.sqldotnet run --project NNews.APIThe API will be available at http://localhost:5007 and https://localhost:5008.
1. Client sends X-Tenant-Id header β 2. TenantMiddleware resolves tenant
β 3. NAuth validates JWT with tenant-specific secret β 4. Request processed
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /article |
List all articles (paginated, filter by categoryId/status) | Yes |
| GET | /article/ListByCategory |
Filter published articles by category & roles | No |
| GET | /article/ListByRoles |
Filter published articles by user roles | No |
| GET | /article/ListByTag |
Filter published articles by tag slug | No |
| GET | /article/Search |
Search articles by keyword | No |
| GET | /article/{id} |
Get article by ID | No |
| POST | /article |
Create new article | Yes |
| POST | /article/insertWithAI |
Create article with AI (ChatGPT) | Yes |
| PUT | /article |
Update article | Yes |
| PUT | /article/updateWithAI |
Update article with AI | Yes |
| DELETE | /article/{id} |
Delete article | Yes |
| GET | /category |
List all categories | Yes |
| GET | /category/listByParent |
Filter categories by parent & roles | No |
| GET | /category/{id} |
Get category by ID | No |
| POST | /category |
Create category | Yes |
| PUT | /category |
Update category | Yes |
| DELETE | /category/{id} |
Delete category | Yes |
| GET | /tag |
List all tags | Yes |
| GET | /tag/ListByRoles |
List tags from published articles | No |
| GET | /tag/{id} |
Get tag by ID | No |
| POST | /tag |
Create tag | Yes |
| PUT | /tag |
Update tag | Yes |
| DELETE | /tag/{id} |
Delete tag | Yes |
| POST | /tag/merge/{sourceId}/{targetId} |
Merge tags | Yes |
| POST | /image/uploadImage |
Upload image (max 100MB) | Yes |
- Database-per-tenant - Each tenant has its own PostgreSQL database
- Per-tenant JWT secrets - Tokens are signed and validated with tenant-specific keys
- TenantId never from body - Only accepted from
X-Tenant-Idheader or JWT claim
- NAuth integration - Centralized authentication and user management
- BasicAuthentication scheme - JWT Bearer with dynamic key resolution
- Role-based access control - Article-level permissions via ArticleRole
pg_dump -U your_user -h localhost -p 5432 -d nnews_emagine_db > backup_emagine.sql
pg_dump -U your_user -h localhost -p 5432 -d nnews_devblog_db > backup_devblog.sqlpsql -U your_user -h localhost -p 5432 -d nnews_emagine_db < backup_emagine.sql
psql -U your_user -h localhost -p 5432 -d nnews_devblog_db < backup_devblog.sqlInstall the NuGet package:
dotnet add package NNewsConfigure in your appsettings.json:
{
"Tenant": {
"DefaultTenantId": "your-tenant-id"
},
"Tenants": {
"your-tenant-id": {
"ConnectionString": "...",
"JwtSecret": "..."
}
}
}Register services with multi-tenant support:
// Register TenantHeaderHandler (auto-injects X-Tenant-Id in all HTTP requests)
services.AddTransient<TenantHeaderHandler>();
// Register ACL clients with tenant propagation
services.AddHttpClient<IArticleClient, ArticleClient>()
.AddHttpMessageHandler<TenantHeaderHandler>();
services.AddHttpClient<ICategoryClient, CategoryClient>()
.AddHttpMessageHandler<TenantHeaderHandler>();Use the ACL (TenantId is propagated automatically):
public class MyService
{
private readonly IArticleClient _articleClient;
public MyService(IArticleClient articleClient)
{
_articleClient = articleClient; // TenantHeaderHandler auto-injects X-Tenant-Id
}
public async Task<List<ArticleDto>> GetArticlesAsync()
{
return await _articleClient.GetAllAsync();
}
}docker-compose up -d --builddocker compose --env-file .env.prod -f docker-compose-prod.yml up --build -dTrigger the production deployment workflow manually from GitHub Actions:
Actions β Deploy Production β Run workflow
| Workflow | Trigger | Description |
|---|---|---|
| Version and Tag | Push to main |
Calculates version via GitVersion and creates git tag |
| Create Release | After Version and Tag | Creates GitHub Release and release branch (for minor/major) |
| Publish NuGet | After Version and Tag | Builds and publishes NNews NuGet package |
| Deploy Production | Manual (workflow_dispatch) |
Deploys to production server via SSH + Docker Compose |
Uses GitVersion (ContinuousDelivery mode). Commit message prefixes control version bumps:
| Prefix | Version Bump |
|---|---|
major: or breaking: |
Major (X.0.0) |
feature: or minor: |
Minor (0.X.0) |
fix: or patch: |
Patch (0.0.X) |
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create a feature branch (
git checkout -b feature/AmazingFeature) - Make your changes
- Commit your changes (
git commit -m 'feature: add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
- Follow Clean Architecture layer separation
- Use Repository Pattern for data access
- Register all dependencies in
Initializer.cs - Use AutoMapper profiles for entity-to-DTO mapping
- Follow commit message conventions for proper versioning
Developed by Rodrigo Landim Carneiro
This project is licensed under the MIT License - see the LICENSE file for details.
- Built with ASP.NET Core 8
- Powered by PostgreSQL
- AI features by OpenAI ChatGPT & DALL-E via zTools
- Authentication by NAuth
- Issues: GitHub Issues
β If you find this project useful, please consider giving it a star!
