BazzucaMedia is a multi-tenant social media management platform for scheduling and publishing content across multiple social networks (X/Twitter, Instagram, Facebook, LinkedIn, TikTok, YouTube). Built with .NET 8 and PostgreSQL, it provides client management, post scheduling with calendar view, media upload via AWS S3, and automated publishing.
The project follows Clean Architecture with six layers and supports per-tenant database isolation. Publishing to X/Twitter is synchronous, while LinkedIn uses an asynchronous architecture with RabbitMQ message queues, retry with TTL, and dead-letter queues (DLQ), processed by a background Worker service using Playwright for browser automation.
- 🏢 Multi-Tenant - Per-tenant database isolation via
X-Tenant-Idheader and JWT claims - 👥 Client Management - CRUD for client accounts with soft-delete
- 📅 Post Scheduling - Calendar-based scheduling with automatic conflict resolution (30-min increments)
- 🌐 Social Network Integration - OAuth credential management for multiple networks per client
- 🐦 X/Twitter Publishing - Synchronous posting with chunked video upload (OAuth 1.0a)
- 💼 LinkedIn Publishing - Asynchronous posting via Playwright browser automation with RabbitMQ queues
- 🔄 Retry & DLQ - Automatic retry with TTL and dead-letter queue for failed LinkedIn posts
- 📦 Media Storage - File upload to AWS S3 via zTools
- 🔐 JWT Authentication - Per-tenant JWT secrets via NAuth
- 🖥️ Debug Console - CLI tool to visually debug LinkedIn publishing with visible Playwright browser
- .NET 8 - Web API, Background Worker, and Console CLI
- Entity Framework Core 9 - ORM with Lazy Loading Proxies
- PostgreSQL - Database (Npgsql provider)
- RabbitMQ - Async message queue for LinkedIn publishing (retry + DLQ topology)
- Microsoft Playwright - LinkedIn publishing via Chromium automation
- NAuth - JWT authentication with
IUserClient - zTools - S3 file upload, ChatGPT, slug generation, email
- AWS S3 - Media file storage
- X/Twitter API - OAuth 1.0a, chunked video upload
- LinkedIn - Web automation via Playwright
- Docker / Docker Compose - Containerized deployment (API, Worker, RabbitMQ, PostgreSQL)
- GitHub Actions - Semantic versioning (GitVersion), release creation, SSH deploy
BazzucaMedia/
├── Bazzuca.API/ # REST API (Controllers, Middlewares, Startup)
├── Bazzuca.Application/ # DI registration (Initializer.cs), Tenant services
├── Bazzuca.Domain/ # Models, Services, Factories (business logic)
├── Bazzuca.DTO/ # Data Transfer Objects, Enums, QueueSettings
├── Bazzuca.Infra/ # DbContext, Repositories, UnitOfWork, RabbitMQ, Playwright
├── Bazzuca.Infra.Interface/ # Repository & service interfaces (zero dependencies)
├── Bazzuca.Worker/ # Background services (LinkedIn RabbitMQ consumer)
├── Bazzuca.Console/ # CLI tool for debugging LinkedIn publishing
├── docs/ # System design diagrams
├── docker-compose.yml # Docker local development
├── docker-compose-prod.yml # Production deployment
├── BazzucaAPI.Dockerfile # API container image
├── BazzucaWorker.Dockerfile # Worker container image (Playwright + Chromium)
└── .github/workflows/ # CI/CD pipelines
API → Application → Domain → Infra.Interface
→ Infra → Infra.Interface
→ DTO (no dependencies)
| Layer | Project | Responsibility |
|---|---|---|
| API | Bazzuca.API |
Controllers, TenantMiddleware, Startup/DI composition |
| Application | Bazzuca.Application |
Initializer.cs (all DI), ITenantContext, ITenantDbContextFactory |
| Domain | Bazzuca.Domain |
Models, Services, Factories, LinkedinService (retry/DLQ) |
| Infra | Bazzuca.Infra |
DbContext, Repositories, RabbitAppService, LinkedinAppService |
| Infra.Interface | Bazzuca.Infra.Interface |
Repository interfaces, IRabbitAppService, ILinkedinAppService |
| DTO | Bazzuca.DTO |
DTOs, Enums, QueueSettings, PublishMessage |
| Worker | Bazzuca.Worker |
LinkedinBackgroundService (RabbitMQ consumer) |
| Console | Bazzuca.Console |
CLI for visual LinkedIn debugging |
The following diagram illustrates the high-level architecture of BazzucaMedia:
Synchronous (X/Twitter): Browser → API → TenantMiddleware → NAuth → PostController → PostService → XService → X/Twitter API
Asynchronous (LinkedIn):
- Enqueue: API → PostController publishes
PublishMessageto RabbitMQ (bazzuca.linkedin.msg) → returns202 Accepted - Process: Worker's
LinkedinBackgroundServiceconsumes message → extracts tenant → creates DI scope → callsLinkedinService.Process() - Publish:
LinkedinServicedownloads media from S3 → callsLinkedinAppService(Playwright) → updates post status - On Failure:
LinkedinServiceincrements retry count → publishes to.retryqueue (TTL returns to.msg) or.errorqueue (DLQ after max retries)
📄 Source: The editable Mermaid source is available at
docs/system-design.mmd.
cp .env.example .env# PostgreSQL
POSTGRES_DB=bazzuca_db
POSTGRES_USER=bazzuca_user
POSTGRES_PASSWORD=your_secure_password_here
POSTGRES_PORT=5434
# Multi-Tenant
DEFAULT_TENANT_ID=bazzuca
BAZZUCA_CONNECTION_STRING=Host=bazzuca-postgres;Port=5432;Database=bazzuca_db;Username=bazzuca_user;Password=your_secure_password_here
BAZZUCA_JWT_SECRET=your_jwt_secret_min_32_chars
# RabbitMQ
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=your_rabbitmq_password_here
# API
API_HTTP_PORT=5010The project uses three environments:
| Environment | Config File | Secrets Source | Swagger |
|---|---|---|---|
| Development | appsettings.Development.json |
Values inline | Yes |
| Docker | appsettings.Docker.json |
.env file |
Yes |
| Production | appsettings.Production.json |
.env.prod file |
No |
# Create external network (first time only)
docker network create emagine-network
# Build and start (API + Worker + RabbitMQ + PostgreSQL)
docker compose up -d --build
# Verify
docker compose ps
docker compose logs -f bazzuca-api
docker compose logs -f bazzuca-worker| Service | URL |
|---|---|
| API | http://localhost:5010 |
| Swagger UI | http://localhost:5010/swagger |
| RabbitMQ Management | http://localhost:15672 (guest/guest) |
| PostgreSQL | localhost:5434 |
| Action | Command |
|---|---|
| Start services | docker compose up -d |
| Start with rebuild | docker compose up -d --build |
| Stop services | docker compose stop |
| View logs | docker compose logs -f |
| View worker logs | docker compose logs -f bazzuca-worker |
| Remove containers | docker compose down |
| Remove with volumes ( |
docker compose down -v |
- .NET 8 SDK
- PostgreSQL 17
- RabbitMQ 3.x (with management plugin)
Create the database and run migrations:
dotnet ef database update --project Bazzuca.Infra --startup-project Bazzuca.API# Via Docker (easiest)
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management-alpinedotnet run --project Bazzuca.APIdotnet run --project Bazzuca.Worker# Opens visible Chromium browser for debugging
dotnet run --project Bazzuca.Console -- --postId 1 --tenantId bazzucaWith the application running, access Swagger UI at: http://localhost:5010/swagger
All endpoints require the Authorization header with a JWT token issued by NAuth. Multi-tenant endpoints also require the X-Tenant-Id header.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /client/listByUser |
List user's clients | Yes |
| GET | /client/getById/{id} |
Get client by ID | Yes |
| POST | /client/insert |
Create client | Yes |
| POST | /client/update |
Update client | Yes |
| DELETE | /client/delete/{id} |
Soft-delete client | Yes |
| GET | /post/listByUser/{month}/{year} |
Calendar month posts | Yes |
| GET | /post/getById/{id} |
Get post by ID | Yes |
| POST | /post/insert |
Create post | Yes |
| POST | /post/update |
Update post | Yes |
| GET | /post/publish/{postId} |
Publish post (sync or async) | Yes |
| POST | /post/search |
Search posts (paginated) | Yes |
| GET | /socialnetwork/listByClient/{id} |
List client networks | Yes |
| POST | /image/upload |
Upload media to S3 | Yes |
Note:
POST /post/publish/{postId}returns200 OKfor X/Twitter (synchronous) or202 Acceptedfor LinkedIn (queued to RabbitMQ).
# Create new migration
dotnet ef migrations add <MigrationName> --project Bazzuca.Infra --startup-project Bazzuca.API
# Apply migrations
dotnet ef database update --project Bazzuca.Infra --startup-project Bazzuca.APIpg_dump -h localhost -p 5434 -U bazzuca_user -d bazzuca_db > backup.sqlpsql -h localhost -p 5434 -U bazzuca_user -d bazzuca_db < backup.sqlProduction deployment uses docker-compose-prod.yml with secrets from .env.prod. In production, PostgreSQL and RabbitMQ are managed externally on the emagine-network — only the API and Worker containers are deployed.
cp .env.prod.example .env.prod
# Edit .env.prod with production secrets (connection string, JWT, RabbitMQ credentials)
docker compose --env-file .env.prod -f docker-compose-prod.yml up --build -d| Workflow | Trigger | Description |
|---|---|---|
| Version and Tag | Push to main |
Auto-generates semantic version tag via GitVersion |
| Create Release | After version tag | Creates GitHub Release and release branch (minor/major only) |
| Deploy Production | Manual dispatch | SSH deploy to production server (API + Worker) |
Check:
docker compose logs rabbitmq
docker compose psCommon causes:
- RabbitMQ container not started or unhealthy
- Incorrect
RabbitMQ:HostNamein appsettings (useemagine-rabbitmqin Docker,localhostlocally)
Check:
# Check worker logs
docker compose logs -f bazzuca-worker
# Check DLQ for failed messages
# Access RabbitMQ Management: http://localhost:15672 → Queues → bazzuca.linkedin.errorCommon causes:
- LinkedIn credentials (user/password) incorrect in SocialNetwork entity
- LinkedIn detected automation (CAPTCHA) — use Console project with
headless: falseto debug - Playwright browser data corrupted — delete
playwright-data/client-{id}/directory
Common causes:
- Queue topology not declared (check worker startup logs)
Queues:LinkedInconfig missing from appsettings
Developed by Rodrigo Landim Carneiro
This project is licensed under the MIT License - see the LICENSE file for details.
