Stack Technique
Vue d'ensemble
La plateforme Roomee utilise un stack technique moderne et éprouvé pour garantir performance, scalabilité et maintenabilité.
Backend
Runtime et Framework
| Technologie | Version | Usage |
|---|---|---|
| Node.js | 18+ | Runtime JavaScript serveur |
| Express.js | 4.x | Framework web léger et flexible |
| TypeScript | 5.x | Services modernes (auth, news, notification) |
| JavaScript | ES6+ | Services legacy (hotel, media, staff-member) |
Migration en cours : Transition progressive des services JavaScript vers TypeScript avec standardisation via @roomee/shared.
Base de Données
MongoDB + Prisma ORM
typescript
// Prisma schema example
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Avantages :
- ✅ Schéma flexible (documents JSON)
- ✅ Performance horizontale (sharding)
- ✅ Type-safety avec Prisma
- ✅ Migrations versionnées
Architecture :
- Une base MongoDB par service (isolation totale)
- Base
memberspartagée pour RBAC (api-staff-member) - Relations via string IDs (pas de foreign keys)
Gestion de Packages
Monorepo avec Turborepo + PNPM
bash
roomee-services/
├── pnpm-workspace.yaml
├── turbo.json
├── packages/
│ └── shared/ # @roomee/shared
└── services/
├── api-authentication/
├── api-hotel/
├── api-staff-member/
├── api-media/
├── api-news/
└── api-notification/Turborepo :
- Cache de build intelligent
- Exécution parallèle des tâches
- Pipeline de dépendances automatique
PNPM :
- Économie d'espace disque (hard links)
- Isolation stricte des dépendances
- Workspaces pour le monorepo
Package Partagé : @roomee/shared
typescript
// Structure du package partagé
@roomee/shared/
├── logger/ // Winston logger configuré
├── amqp/ // Client AMQP/RabbitMQ
├── socket/ // Socket.IO server wrapper
├── auth/ // JWT middleware + encryption
├── encryption/ // AES-256-GCM encryption
├── middlewares/ // Error handlers, validation
└── utils/ // Helpers, constantsExports :
typescript
import {
authMiddleware,
encryptionService,
logger,
amqpServer,
socketServer,
errorHandler,
catchAsync,
ApiError
} from '@roomee/shared'Communication Inter-Services
1. REST API (Synchrone)
Express.js + Axios
typescript
// Serveur Express
app.use(express.json())
app.use(helmet())
app.use(cors({ origin: allowedOrigins }))
// Appels HTTP entre services
const response = await axios.post(
`${MEMBER_API_URL}/members/add`,
memberData,
{
headers: {
Authorization: `Bearer ${token}`,
'X-API-Key': apiKey
}
}
)Middlewares standards :
helmet: Sécurité headers HTTPcors: Cross-Origin Resource Sharingmorgan: Logging HTTPexpress-rate-limit: Rate limitingexpress-mongo-sanitize: Protection NoSQL injection
2. AMQP / RabbitMQ (Asynchrone)
Event Bus avec amqplib
typescript
// Configuration
amqpServer.initialize({
gatewayUrl: 'amqp://localhost:5672',
exchangeName: 'roomee_events',
queueName: 'notification_queue',
routingKeyBase: 'roomee.notification'
})
// Publier un événement
await amqpServer.emit('roomee.notification.page.created', {
type: 'page.created',
workspaceId: '...',
payload: pageData
})
// Consommer des événements
await amqpServer.subscribe('roomee.notification.*', async (message) => {
await handleNotification(message)
})Features :
- Topic exchange pour routing flexible
- Dead Letter Queue pour messages en échec
- Retry avec backoff exponentiel (3 tentatives)
- Prefetch pour contrôle de charge
- Acknowledgment manuel pour fiabilité
3. Socket.IO (Temps Réel)
WebSocket avec JWT Authentication
typescript
// Serveur Socket.IO
socketServer.initialize({
server: httpServer,
jwtSecret: config.jwtAccessSecret,
corsOrigin: config.frontendUrl
})
// Rooms pour isolation multi-tenant
socket.join(`workspace:${workspaceId}`)
socket.join(`member:${memberId}`)
// Émettre un événement
io.to(`workspace:${workspaceId}`).emit('page.created', pageData)
// Client
socket.on('page.created', (data) => {
console.log('Nouvelle page créée:', data)
})Rooms utilisées :
member:{memberId}- Notifications personnellesworkspace:{workspaceId}- Workspace collaboratifecosystem:{ecosystemId}- Niveau organisationpage:{pageId}- Page spécifique
Authentification et Sécurité
JWT avec Encryption AES-256-GCM
typescript
// Génération de token chiffré
const token = authMiddleware.generateToken({
userId: user.id,
email: user.email,
workspaceId: workspace.id,
roles: ['admin'],
permissions: ['manage_members']
})
// Format du token : {encryptedText}.{iv}.{tag}
// Algorithme : AES-256-GCMMiddleware d'authentification :
typescript
// Protéger une route
router.use(authMiddleware.middleware)
// Le token décrypté est disponible dans req.user
router.get('/profile', (req, res) => {
const { userId, workspaceId } = req.user
// ...
})Refresh Tokens
typescript
// Rotation des tokens
model RefreshToken {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String @db.ObjectId
token String @unique
expiresAt DateTime
isRevoked Boolean @default(false)
}
// Flow de refresh
POST /auth/refresh
Body: { refreshToken: "..." }
Response: { accessToken: "...", refreshToken: "..." }Protection des API
typescript
// Rate Limiting par endpoint
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 20, // 20 requêtes max
message: 'Trop de tentatives de connexion'
})
app.use('/auth/login', authLimiter)
// NoSQL Injection Protection
app.use(mongoSanitize())
// XSS Protection
app.use(helmet.contentSecurityPolicy())Validation et Typage
Zod (Services Modernes)
typescript
import { z } from 'zod'
// Définir un schema
const createPageSchema = z.object({
title: z.string().min(1).max(255),
content: z.any(), // Editor.js JSON
workspaceId: z.string().regex(/^[0-9a-fA-F]{24}$/),
categoryId: z.string().optional(),
enableNotifications: z.boolean().default(true)
})
// Middleware de validation
const validate = (schema: z.ZodSchema) => {
return (req, res, next) => {
try {
schema.parse(req.body)
next()
} catch (error) {
res.status(400).json({ error: error.errors })
}
}
}
// Utilisation
router.post('/', validate(createPageSchema), controller.create)Validation Legacy (Joi)
typescript
// Services JS utilisent Joi
const schema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required()
})
const { error, value } = schema.validate(req.body)Stockage et Médias
Google Cloud Storage (GCP)
typescript
import { Storage } from '@google-cloud/storage'
// Configuration
const storage = new Storage({
projectId: config.gcpProjectId,
keyFilename: config.gcpKeyFile
})
const bucket = storage.bucket(config.gcpBucketName)
// Upload direct avec presigned URL
const [url] = await bucket.file(filename).getSignedUrl({
version: 'v4',
action: 'write',
expires: Date.now() + 15 * 60 * 1000, // 15 minutes
contentType: mimeType
})
// Client upload directement vers GCP (pas de transit serveur)
await fetch(url, {
method: 'PUT',
body: file,
headers: { 'Content-Type': mimeType }
})Fonctionnalités :
- Upload direct via presigned URLs
- Thumbnails automatiques (Sharp)
- Conversion de formats (WebP, JPEG)
- Quota par écosystème (20GB Premium)
- Chunked upload pour mobile (50MB max)
Notifications Multi-Canal
Push Notifications (OneSignal)
typescript
import OneSignal from 'onesignal-node'
const client = new OneSignal.Client({
appId: config.oneSignalAppId,
restApiKey: config.oneSignalApiKey
})
// Envoyer notification push
await client.createNotification({
include_player_ids: subscriptionIds,
headings: { en: title },
contents: { en: message },
data: { type, entityId, workspaceId }
})Email (SendGrid + Nodemailer)
typescript
import sgMail from '@sendgrid/mail'
sgMail.setApiKey(config.sendgridApiKey)
// Templates avec variables
await sgMail.send({
to: email,
from: 'noreply@roomee.com',
templateId: 'invitation_template',
dynamicTemplateData: {
memberName: 'John Doe',
workspaceName: 'Mon Workspace',
invitationLink: 'https://...'
}
})Firebase Cloud Messaging (FCM)
typescript
import admin from 'firebase-admin'
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
})
// Envoyer notification à un device
await admin.messaging().send({
token: deviceToken,
notification: {
title: 'Nouvelle actualité',
body: 'Un article a été publié'
},
data: {
type: 'page.created',
pageId: '...'
}
})Logging et Monitoring
Winston Logger
typescript
import winston from 'winston'
import 'winston-daily-rotate-file'
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
// Console (dev)
new winston.transports.Console({
format: winston.format.colorize()
}),
// Fichiers rotatifs (prod)
new winston.transports.DailyRotateFile({
filename: 'logs/error-%DATE%.log',
level: 'error',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d'
}),
new winston.transports.DailyRotateFile({
filename: 'logs/combined-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d'
})
]
})
// Utilisation
logger.info('User logged in', { userId, email })
logger.error('Database error', { error, context })Sentry (Error Tracking)
typescript
import * as Sentry from '@sentry/node'
Sentry.init({
dsn: config.sentryDsn,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0
})
// Capture d'erreur
try {
// Code
} catch (error) {
Sentry.captureException(error)
logger.error('Error', { error })
}
// Middleware Express
app.use(Sentry.Handlers.requestHandler())
app.use(Sentry.Handlers.errorHandler())Testing
Jest (Tests Unitaires)
typescript
// jest.config.js
export default {
preset: 'ts-jest',
testEnvironment: 'node',
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
}
// Example test
describe('MemberService', () => {
it('should create a member', async () => {
const member = await memberService.create(memberData)
expect(member).toHaveProperty('id')
expect(member.email).toBe(memberData.email)
})
})Supertest (Tests d'Intégration)
typescript
import request from 'supertest'
import app from '../src/app'
describe('POST /auth/login', () => {
it('should return JWT token', async () => {
const response = await request(app)
.post('/auth/login')
.send({ email: 'test@example.com', password: 'password' })
.expect(200)
expect(response.body).toHaveProperty('accessToken')
})
})Documentation API
Swagger / OpenAPI
typescript
import swaggerJsdoc from 'swagger-jsdoc'
import swaggerUi from 'swagger-ui-express'
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Roomee API',
version: '1.0.0',
description: 'API documentation'
},
servers: [
{ url: 'http://localhost:3000', description: 'Development' },
{ url: 'https://api.roomee.io', description: 'Production' }
]
},
apis: ['./src/routes/*.ts']
}
const swaggerSpec = swaggerJsdoc(options)
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec))Annotations JSDoc :
typescript
/**
* @swagger
* /api/v1/pages:
* post:
* summary: Créer une page
* tags: [Pages]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* title:
* type: string
* content:
* type: object
* responses:
* 201:
* description: Page créée
*/Intégrations Externes
Sendbird (Chat)
typescript
import axios from 'axios'
const sendbirdApi = axios.create({
baseURL: 'https://api-{app_id}.sendbird.com/v3',
headers: {
'Api-Token': config.sendbirdApiToken
}
})
// Créer une application chat
await sendbirdApi.post('/applications', {
organization_id: orgId,
name: hotelName
})Stripe (Paiements)
typescript
import Stripe from 'stripe'
const stripe = new Stripe(config.stripeSecretKey, {
apiVersion: '2023-10-16'
})
// Créer un abonnement
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }]
})OpenWeather (Météo)
typescript
const weather = await axios.get(
`https://api.openweathermap.org/data/2.5/weather`,
{
params: {
q: city,
appid: config.openWeatherApiKey,
units: 'metric'
}
}
)Déploiement
Docker
dockerfile
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# Install PNPM
RUN npm install -g pnpm
# Copy package files
COPY package.json pnpm-lock.yaml ./
COPY packages/shared packages/shared
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy service code
COPY services/api-authentication services/api-authentication
# Build
RUN pnpm run build
EXPOSE 3000
CMD ["node", "services/api-authentication/dist/server.js"]Kubernetes
yaml
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-authentication
spec:
replicas: 3
selector:
matchLabels:
app: api-authentication
template:
metadata:
labels:
app: api-authentication
spec:
containers:
- name: api-authentication
image: roomee/api-authentication:latest
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secrets
key: urlCI/CD (GitHub Actions)
yaml
name: Deploy Service
on:
push:
branches: [main]
paths:
- 'services/api-authentication/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm test
- name: Build
run: pnpm build
- name: Build Docker image
run: docker build -t roomee/api-authentication:latest .
- name: Push to registry
run: docker push roomee/api-authentication:latest
- name: Deploy to K8s
run: kubectl apply -f k8s/Gestion de Configuration
Infisical (Secrets Management)
typescript
import { InfisicalClient } from '@infisical/sdk'
const client = new InfisicalClient({
siteUrl: config.infisicalUrl
})
// Charger les secrets
const secrets = await client.listSecrets({
environment: process.env.NODE_ENV,
projectId: config.infisicalProjectId,
path: '/COMMON'
})
// Mapper vers process.env
secrets.forEach(({ key, value }) => {
process.env[key] = value
})Hiérarchie des secrets :
COMMON/ # Variables partagées (DB, JWT)
AMQP/ # Configuration RabbitMQ
Customer/ # Variables par service
Chat/ # SendbirdPerformance et Optimisation
Indexes MongoDB
typescript
// Prisma indexes
model Page {
@@index([workspaceId, deleted_at])
@@index([createdById])
@@index([categoryId])
}Caching (à implémenter)
typescript
import Redis from 'ioredis'
const redis = new Redis({
host: config.redisHost,
port: 6379
})
// Cache des permissions RBAC
const cacheKey = `permissions:${memberId}:${workspaceId}`
let permissions = await redis.get(cacheKey)
if (!permissions) {
permissions = await fetchPermissions(memberId, workspaceId)
await redis.setex(cacheKey, 300, JSON.stringify(permissions)) // TTL 5min
}Résumé du Stack
| Catégorie | Technologies |
|---|---|
| Runtime | Node.js 18+, TypeScript 5.x |
| Framework | Express.js 4.x |
| Database | MongoDB + Prisma ORM |
| Monorepo | Turborepo + PNPM |
| Communication | REST (Axios), AMQP (RabbitMQ), Socket.IO |
| Auth | JWT + AES-256-GCM |
| Validation | Zod, Joi |
| Storage | Google Cloud Storage |
| Notifications | OneSignal, FCM, SendGrid |
| Chat | Sendbird |
| Payments | Stripe |
| Logging | Winston, Sentry |
| Testing | Jest, Supertest |
| Docs | Swagger/OpenAPI |
| Deployment | Docker, Kubernetes |
| CI/CD | GitHub Actions |
| Secrets | Infisical |