Architecture Globale
Vue d'ensemble
Roomee est construit sur une architecture microservices moderne dans un monorepo, orchestrée par Turborepo et gérée avec PNPM. Cette architecture permet une scalabilité, une maintenabilité et un déploiement indépendant de chaque service.
Principes Architecturaux
1. Monorepo Multi-Services
roomee-services/
├── packages/
│ └── shared/ # @roomee/shared - Code partagé
│ ├── logger/
│ ├── amqp/
│ ├── socket/
│ ├── auth/
│ └── encryption/
├── services/
│ ├── api-authentication/ # 🔐 Auth & Users
│ ├── api-hotel/ # 🏨 Ecosystems
│ ├── api-staff-member/ # 👥 Members & RBAC
│ ├── api-media/ # 📁 Storage & Media
│ ├── api-news/ # 📰 CMS
│ └── api-notification/ # 🔔 Notifications
└── turbo.json2. Architecture Event-Driven
graph TB
subgraph "Frontend"
WEB[Web App<br/>React]
MOBILE[Mobile App<br/>React Native]
end
subgraph "API Layer"
AUTH[api-authentication]
HOTEL[api-hotel]
MEMBER[api-staff-member]
NEWS[api-news]
MEDIA[api-media]
NOTIF[api-notification]
end
subgraph "Infrastructure"
AMQP[RabbitMQ<br/>Event Bus]
MONGO[(MongoDB<br/>Per Service)]
GCP[GCP Storage]
end
WEB -.REST + WebSocket.-> AUTH
WEB -.REST + WebSocket.-> HOTEL
WEB -.REST + WebSocket.-> MEMBER
WEB -.REST + WebSocket.-> NEWS
MOBILE -.REST + WebSocket.-> AUTH
MOBILE -.REST + WebSocket.-> MEMBER
AUTH --> AMQP
HOTEL --> AMQP
MEMBER --> AMQP
NEWS --> AMQP
MEDIA --> AMQP
NOTIF -.Consumer.-> AMQP
AUTH --> MONGO
HOTEL --> MONGO
MEMBER --> MONGO
NEWS --> MONGO
NOTIF --> MONGO
MEDIA --> GCPTypes de Communication
Communication Synchrone (REST)
Usage : API publiques, opérations CRUD
| Service Source | Service Cible | Endpoint | Usage |
|---|---|---|---|
| api-hotel | api-staff-member | POST /members/add/hotel/member | Créer administrateur hôtel |
| api-hotel | api-stripe | POST /sub/assign-subscription/ | Créer abonnement |
| api-authentication | api-staff-member | GET /members/get/with/:email | Récupérer profil membre |
| api-news | api-staff-member | POST /members/sync | Synchroniser membres |
Avantages :
- Réponse immédiate
- Simplicité d'implémentation
- Debugging facile
Inconvénients :
- Couplage fort
- Disponibilité requise
- Latence potentielle
Communication Asynchrone (AMQP)
Usage : Événements métier, notifications, synchronisation
Routing Keys :
roomee.notification.{module}.{action}
Exemples :
- roomee.notification.page.created
- roomee.notification.member.registered
- roomee.notification.workspace.member_joined
- roomee.notification.comment.createdPattern Pub/Sub :
// Émetteur (api-news)
await amqpServer.emit('roomee.notification.page.created', {
type: 'page.created',
workspaceId: '...',
payload: pageData,
channels: ['push', 'socket'],
metadata: {
actor: { id, name },
receiverIds: [...]
}
})
// Consommateur (api-notification)
amqpServer.subscribe('roomee.notification.page.*', async (message) => {
await notificationProcessor.process(message)
})Avantages :
- Découplage total
- Scalabilité
- Résilience (retry, DLQ)
Inconvénients :
- Complexité accrue
- Éventuelle consistance
- Debugging plus complexe
Communication Temps Réel (Socket.IO)
Usage : Mises à jour UI temps réel
Rooms et Événements :
// Connexion avec auth JWT
socket.on('connect', () => {
socket.emit('joinWorkspace', workspaceId)
socket.emit('subscribeToTypes', {
workspaceId,
types: ['page', 'comment', 'notification']
})
})
// Réception événements
socket.on('page.created', (data) => { ... })
socket.on('notification:new', (data) => { ... })
socket.on('member.registered', (data) => { ... })Rooms disponibles :
member:{memberId}- Personnelworkspace:{workspaceId}- Workspaceecosystem:{ecosystemId}- Écosystèmeworkspace:{workspaceId}:type:{type}- Par typepage:{pageId}- Page spécifique
Isolation Multi-Tenant
Niveaux d'isolation
Plateforme Roomee
└── Écosystème (hotelId / ecosystemId)
└── Workspace (workspaceId)
└── Membres (memberId)Filtrage des données
Toutes les requêtes doivent filtrer par :
- workspaceId (isolation workspace)
- ecosystemId (isolation écosystème)
- deleted_at IS NULL (soft delete)
// ✅ BON
const pages = await prisma.page.findMany({
where: {
workspaceId: req.user.workspaceId,
deleted_at: null
}
})
// ❌ MAUVAIS (fuite multi-tenant)
const pages = await prisma.page.findMany({
where: { deleted_at: null }
})Permissions RBAC
Système à 2 niveaux :
Permissions Écosystème (generales)
access_live_chatscreenshot_permissionmanage_ecosystem
Permissions Workspace (role-based)
- Définies par
workspaceRole - Stockées dans
permissionSlugs[] - Catégorisées : members, content, settings, communication
- Définies par
Gestion des Erreurs
Hiérarchie des erreurs (@roomee/shared)
AppError (base)
├── ValidationError (400)
├── UnauthorizedError (401)
├── ForbiddenError (403)
├── NotFoundError (404)
├── ConflictError (409)
├── TooManyRequestsError (429)
├── InternalServerError (500)
├── ServiceUnavailableError (503)
├── DatabaseError (500)
└── ExternalServiceError (502)Gestion centralisée
// Middleware error handler
app.use(errorConverter)
app.use(errorHandler)
// Capture Sentry
sentryService.captureException(error)
// Logs Winston
logger.error('Error processing request', { error, context })Monitoring et Observabilité
Logging (Winston)
Niveaux : error, warn, info, debug
logger.info('User logged in', { userId, email })
logger.error('Database connection failed', { error })Transports :
- Console (dev)
- Daily rotate file (prod)
- MongoDB (analytics)
- Discord webhook (alertes)
Error Tracking (Sentry)
sentryService.initialize({
dsn: process.env.SENTRY_DSN,
environment: 'production',
tracesSampleRate: 1.0
})Health Checks
GET /health
{
"status": "UP",
"service": "api-authentication",
"timestamp": "2025-10-31T12:00:00.000Z"
}Sécurité
Authentification JWT
Token chiffré :
// Format : {encryptedText}.{iv}.{tag}
const token = authMiddleware.generateToken({
userId,
email,
workspaceId,
roles,
permissions
})Validation :
// Middleware @roomee/shared
router.use(authMiddleware.middleware)Protection des API
// Helmet (headers sécurité)
app.use(helmet())
// CORS
app.use(cors({ origin: allowedOrigins }))
// NoSQL Injection
app.use(mongoSanitize())
// Rate Limiting
app.use('/auth', rateLimiter({
windowMs: 15 * 60 * 1000,
max: 20
}))Validation des entrées
Zod schemas (services modernes) :
const createPageSchema = z.object({
title: z.string().min(1).max(255),
content: z.any(),
workspaceId: z.string().regex(/^[0-9a-fA-F]{24}$/)
})
router.post('/', validate(createPageSchema), controller)Scalabilité
Stratégies
Horizontal Scaling
- Chaque service peut être répliqué
- Load balancing via Nginx/Traefik
Database Sharding
- Une base MongoDB par service
- Possibilité de sharding par workspaceId
AMQP Queue Workers
- Plusieurs consumers par queue
- Prefetch pour contrôler la charge
Caching (à implémenter)
- Redis pour les permissions
- TTL 5 minutes
Déploiement
Environnements
| Env | Infra | Branch | URL |
|---|---|---|---|
| local | Docker Compose | - | localhost |
| dev | K8s Dev | develop | dev.roomee.io |
| staging | K8s Staging | staging | staging.roomee.io |
| prod | K8s Prod | main | api.roomee.io |
CI/CD Pipeline
# .github/workflows/deploy.yml
name: Deploy Service
on:
push:
paths:
- 'services/api-authentication/**'
branches: [main, staging, develop]
jobs:
deploy:
- Install dependencies (pnpm)
- Build @roomee/shared
- Build service
- Run tests
- Build Docker image
- Push to registry
- Deploy to K8sConfiguration Secrets (Infisical)
Hiérarchie :
COMMON/ # Variables partagées (DB, JWT, API_BASE_URL)
AMQP/ # Configuration RabbitMQ
Customer/ # Variables spécifiques au service
Chat/ # Sendbird, etc.// Chargement automatique
const config = await configService.configureEnvVar()Bonnes Pratiques
Repository Pattern
// Controller → Service → Repository → Prisma → DB
const member = await memberRepository.findById(id)Soft Delete
// Toujours soft delete
await prisma.page.update({
where: { id },
data: { deleted_at: new Date(), deletedById }
})
// Toujours filtrer
where: { deleted_at: null }Pagination
const { page = 1, limit = 20 } = req.query
const skip = (page - 1) * limit
const [items, total] = await Promise.all([
prisma.page.findMany({ skip, take: limit }),
prisma.page.count()
])
return {
data: items,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit)
}
}Transactions
await prisma.$transaction(async (tx) => {
const page = await tx.page.create({ data })
await tx.notification.create({ data: { pageId: page.id } })
})Évolutions Futures
Court Terme
- [ ] API Gateway (Kong/Traefik)
- [ ] Cache Redis global
- [ ] GraphQL Federation
- [ ] Swagger UI centralisé
Moyen Terme
- [ ] Service Mesh (Istio)
- [ ] Distributed Tracing (Jaeger)
- [ ] Feature Flags (LaunchDarkly)
Long Terme
- [ ] Event Sourcing
- [ ] CQRS Pattern
- [ ] Kubernetes Federation multi-cloud