Conception complete (Phase 0) pour formation-hub Acadenice : - 19 docs Merise Agile + UML + GitOps + plans (tests/deploy/ops/api) cf docs/00-readme.md pour l'index complet - Stack Docker compose (Docmost + Baserow + Postgres + Redis + MinIO local FS) compose.yml + compose.staging.yml + compose.prod.yml - CI/CD GitHub Actions skeleton (ci, deploy-staging, deploy-prod) - Bridge service skeleton (Hono + TS + Biome + Vitest + zod + pino) - Templates GitHub : PR + 3 issue types + CODEOWNERS + dependabot.yml - Scripts ops : healthcheck, backup quotidien, smoke-test post-deploy - LICENSE AGPL-3.0 + SECURITY.md + CONTRIBUTING.md + CHANGELOG.md - Diagramme drawIO archi infra (XML importable dans diagrams.net) Decisions structurelles enregistrees : - Scope CFA + Agence avec entite PERSONNE pivot multi-roles (ADR-001) - Stack composite Docmost AGPL + Baserow MIT + bridge custom (ADR-001) - Path B : UX quasi-unified via Tiptap node-views custom (ADR-002) - Monorepo trunk-based development (ADR-003) - Postgres separe Docmost/Baserow (ADR-004) - Bridge stack Node 22 + Hono (ADR-005) - Repo neuf prefere a fork Docmost - Prod-like des le jour 1 (pas MVP)
18 KiB
Bridge API Design
Specification du bridge service : architecture, endpoints, auth, contrats, integration patterns. Service Node TS qui expose Baserow comme nodes Tiptap custom dans Docmost et orchestre les rollups cross-zone. Statut : design doc avant code Phase 2.
1. Mission du bridge
Le bridge est notre seul code custom. Il a 4 missions :
- Exposer les rows Baserow comme objets typed au reste de l'ecosysteme (typing strict, validation, cache)
- Recevoir les webhooks Baserow pour invalider caches et declencher actions (notifications, recalculs cross-zone)
- Servir les Tiptap node-views custom dans Docmost (mention
@formateur, embed[projet], etc.) avec donnees fraiches - Orchestrer les workflows metier que ni Docmost ni Baserow ne savent faire seuls (validation RG, notifications croisees, capacite Personne unifiee)
Le bridge ne stocke pas d'etat metier (Phase 2). Source of truth = Baserow. Le bridge est stateless avec cache Redis.
2. Tech stack
| Composant | Choix | Justification |
|---|---|---|
| Runtime | Node 22 LTS | Stable, ecosysteme TS mature |
| Framework HTTP | Hono | Leger, performant, TypeScript-first, edge-ready si futur |
| Validation | Zod | Schemas TS-typed, runtime validation |
| HTTP client | ofetch | Wrapper fetch avec retry, timeout, JSON typing |
| Cache | Redis 7 | Partage avec Docmost ou dedie (decision Phase 2) |
| Tests | Vitest + testcontainers | Cf doc 16 |
| Logger | Pino | Structured JSON logs, perf |
| Config | dotenv + zod | .env parse + valide au boot |
| Build | TypeScript native + bundling esbuild | Pas Webpack, simple |
| Deploy | Docker image multi-stage | Image < 100 Mo |
3. Architecture interne
flowchart TB
subgraph "Bridge service (Hono)"
Routes[Routes layer<br/>endpoints REST + webhooks]
Middleware[Middleware<br/>auth, logging, rate-limit, error]
Services[Services layer<br/>PersonneService, ProjetService, etc.]
Adapters[Adapters layer<br/>BaserowClient, DocmostClient, RedisCache]
Domain[Domain layer<br/>Personne, Module, Tache classes pures]
end
Routes --> Middleware
Middleware --> Services
Services --> Domain
Services --> Adapters
Adapters -->|HTTP| Baserow[(Baserow API)]
Adapters -->|HTTP| Docmost[(Docmost API)]
Adapters -->|TCP| Redis[(Redis)]
Layers :
- Routes : declaration endpoints + Zod schemas + delegation services
- Middleware : transverse (auth, logs, rate-limiting, error handling)
- Services : logique metier orchestree (Use Cases level)
- Domain : classes pures (Personne, Module, Attribution... cf doc 12)
- Adapters : isolation IO (Baserow API, Docmost API, Redis)
4. Conventions API REST
4.1 Style
- REST-ish : endpoints orientes resources, verbes HTTP standards (GET, POST, PUT, PATCH, DELETE)
- JSON uniquement (request + response)
- Response shape standard :
// Success
{ "data": <payload>, "meta"?: { ... } }
// Error
{ "error": { "code": "ERR_CODE", "message": "Human readable", "details"?: {...} } }
4.2 Naming
- Plural noms :
/personnes,/projets,/attributions - IDs en path :
/personnes/:id - Sub-resources :
/projets/:id/taches - Actions : verb-style en POST si non-CRUD :
/attributions/:id/cloturer
4.3 Versioning
- Prefix
/api/v1/sur toutes les routes - Breaking change → nouvelle version
/api/v2/(en parallele pendant transition) - Deprecations annoncees minimum 3 mois avant retrait
4.4 Pagination, filtre, tri (pour les list endpoints)
GET /api/v1/personnes?
page=1& # default 1
per_page=50& # default 50, max 200
sort=nom& # default id desc
filter[role]=formateur&
filter[statut]=actif
Response :
{
"data": [...],
"meta": {
"page": 1,
"per_page": 50,
"total": 127,
"total_pages": 3
}
}
5. Authentification
5.1 Strategies
| Type | Usage | Header |
|---|---|---|
| API Token longue duree | Service-to-service (Docmost ↔ Bridge, Cron ↔ Bridge) | Authorization: Bearer brg_<token> |
| JWT court (Phase 3+) | User authentifie via Docmost SSO | Authorization: Bearer <jwt> |
| Webhook signature | Verification webhook Baserow | X-Baserow-Signature: <hmac-sha256> |
5.2 Generation tokens
API tokens generes via CLI bridge :
npm run --prefix bridge token:create -- --name "docmost-prod" --scopes "read:* write:attributions"
# → "brg_a1b2c3d4..." stocke en .env.prod cote Docmost
Tokens stockes en clair dans une table api_tokens Postgres (Phase 3+) ou en memoire au boot via .env (Phase 2 simple).
5.3 Scopes
| Scope | Permissions |
|---|---|
read:personnes |
GET /personnes/* |
read:projets |
GET /projets/* |
write:attributions |
POST/PATCH /attributions |
write:interventions |
POST/PATCH /interventions |
webhook:baserow |
POST /webhooks/baserow/* |
admin:* |
Tout (Corentin/Yan tokens) |
6. Endpoints REST
6.1 Personnes
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/personnes |
read:personnes |
Liste paginee, filtrable par role/statut |
| GET | /api/v1/personnes/:id |
read:personnes |
Fiche detail avec heures restantes (formation + agence + total) |
| GET | /api/v1/personnes/:id/attributions |
read:personnes |
Attributions actives + historiques |
| GET | /api/v1/personnes/:id/interventions |
read:personnes |
Interventions sur taches (paginees) |
| GET | /api/v1/personnes/:id/dashboard |
read:personnes |
Vue 360 : capacite, attributions, interventions, projets en cours |
6.2 Formations / Blocs / Modules
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/formations |
read:formations |
Liste paginee |
| GET | /api/v1/formations/:id |
read:formations |
Detail avec blocs/modules + rollups |
| GET | /api/v1/blocs/:id |
read:formations |
Detail bloc + modules |
| GET | /api/v1/modules/:id |
read:formations |
Detail module + attributions actives |
| POST | /api/v1/modules/:id/attribuer |
write:attributions |
Cree une attribution avec validation RG |
6.3 Attributions
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/attributions/:id |
read:attributions |
Detail |
| PATCH | /api/v1/attributions/:id/heures-realisees |
write:attributions |
Saisir heures realisees (UC-13) |
| POST | /api/v1/attributions/:id/cloturer |
write:attributions |
Statut → realise |
| POST | /api/v1/attributions/:id/annuler |
write:attributions |
Statut → annule (justification requise) |
6.4 Clients / Projets / Taches
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/clients |
read:projets |
Liste |
| GET | /api/v1/clients/:id |
read:projets |
Detail + projets |
| GET | /api/v1/projets |
read:projets |
Liste filtrable par statut/client |
| GET | /api/v1/projets/:id |
read:projets |
Detail + taches + heures realisees rollup |
| GET | /api/v1/projets/:id/timeline |
read:projets |
Vue chronologique interventions |
| GET | /api/v1/taches/:id |
read:projets |
Detail + interventions |
6.5 Interventions
| Method | Path | Scope | Description |
|---|---|---|---|
| POST | /api/v1/interventions |
write:interventions |
Saisir intervention (UCA-07) |
| PATCH | /api/v1/interventions/:id |
write:interventions |
Edit (heures, notes) |
| POST | /api/v1/interventions/:id/annuler |
write:interventions |
Annulation |
6.6 Rapports
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/rapports/formation/:id?format=pdf |
read:formations |
PDF rapport formation |
| GET | /api/v1/rapports/personne/:id?format=pdf |
read:personnes |
PDF rapport personne (heures + attributions) |
| GET | /api/v1/rapports/projet/:id?format=pdf |
read:projets |
PDF rapport projet |
6.7 Health & metrics
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/health |
(none) | Healthcheck (200 si OK, 503 si degraded) |
| GET | /api/ready |
(none) | Readiness (Baserow + Redis joignables) |
| GET | /api/metrics |
admin:* |
Prometheus metrics format |
7. Webhooks Baserow
Baserow envoie des webhooks sur les changements de rows. Bridge traite et reagit.
7.1 Endpoints webhook
| Method | Path | Description |
|---|---|---|
| POST | /api/webhooks/baserow/attribution-changed |
Row created/updated/deleted sur table attribution |
| POST | /api/webhooks/baserow/intervention-changed |
Idem intervention |
| POST | /api/webhooks/baserow/module-status-changed |
Quand module_statut change |
| POST | /api/webhooks/baserow/projet-status-changed |
Quand projet_statut change |
7.2 Format payload Baserow (extrait)
{
"table_id": 123,
"database_id": 1,
"event_type": "rows.created",
"items": [
{ "id": 42, "field_X": "...", ... }
]
}
7.3 Verification signature
// middleware/webhook-baserow.ts
const expected = hmacSha256(rawBody, env.BASEROW_WEBHOOK_SECRET);
const provided = req.headers['X-Baserow-Signature'];
if (!constantTimeEqual(expected, provided)) {
throw new Error('Invalid signature');
}
7.4 Actions par webhook
| Webhook | Actions |
|---|---|
| attribution-changed | Invalide cache Redis personne/module concernes ; si statut change vers realise ou annule → recalcul rollup module ; notif email formateur si nouvelle attribution |
| intervention-changed | Invalide cache personne/tache ; check capacite Personne, alerte si depassement |
| module-status-changed | Si tous modules d'une formation realise → declenche cloture formation auto (OP-07) |
| projet-status-changed | Si livre → notif admin pour facturation |
7.5 Idempotence
Chaque webhook a un event_id. Le bridge stocke en Redis avec TTL 24h les events deja traites :
const seen = await redis.get(`webhook:event:${event_id}`);
if (seen) return; // skip duplicate
await redis.set(`webhook:event:${event_id}`, '1', 'EX', 86400);
8. Cache strategy
8.1 Cles Redis
| Cle | TTL | Contenu |
|---|---|---|
bridge:personne:<id> |
5 min | JSON full Personne (avec rollups calcules) |
bridge:projet:<id> |
5 min | JSON full Projet |
bridge:formation:<id> |
10 min | JSON Formation (change moins souvent) |
bridge:webhook:event:<event_id> |
24h | Idempotence webhook |
bridge:rate-limit:<token>:<endpoint> |
1 min | Rate limit counter |
8.2 Invalidation
- Webhook Baserow invalide les cles concernees
- TTL comme fallback (5-10 min)
- Pattern :
cache.invalidate('bridge:personne:42')apres write
8.3 Cache aside pattern
async getPersonne(id: number): Promise<Personne> {
const cached = await cache.get(`bridge:personne:${id}`);
if (cached) return Personne.fromJSON(cached);
const fresh = await baserow.fetchPersonne(id);
await cache.set(`bridge:personne:${id}`, fresh.toJSON(), 'EX', 300);
return fresh;
}
9. Rate limiting
Par token + endpoint (sliding window 1 min) :
| Endpoint | Limite |
|---|---|
| Read endpoints | 600 req/min |
| Write endpoints | 60 req/min |
| Webhooks | 1000 req/min |
| Rapports PDF | 10 req/min |
Reponse 429 si depasse :
{ "error": { "code": "RATE_LIMITED", "message": "Too many requests", "retry_after": 30 } }
10. Error handling
10.1 Codes d'erreur
| Code | HTTP | Description |
|---|---|---|
AUTH_REQUIRED |
401 | Token absent |
AUTH_INVALID |
401 | Token invalide |
FORBIDDEN_SCOPE |
403 | Token n'a pas le scope requis |
NOT_FOUND |
404 | Ressource inexistante |
VALIDATION_ERROR |
400 | Body invalide (Zod errors) |
RG_VIOLATION |
422 | Regle de gestion violee (ex RG-01 depassement heures module) |
CONFLICT |
409 | Etat incoherent (ex annuler une attribution deja annulee) |
RATE_LIMITED |
429 | Trop de requetes |
BASEROW_UNAVAILABLE |
502 | Baserow API down |
INTERNAL |
500 | Bug bridge |
10.2 Format
{
"error": {
"code": "RG_VIOLATION",
"message": "Heures attribuees depassent la capacite du module",
"details": {
"rule": "RG-01",
"module_id": 42,
"heures_module": 30,
"heures_deja_attribuees": 28,
"heures_demandees": 5
}
}
}
11. Integration patterns Docmost
11.1 Tiptap node-view custom
Phase 2+ : on developpe (ou on commande a un freelance) des extensions Tiptap pour Docmost qui appellent le bridge.
Patterns :
- Mention
@formateur:Pierre→ render carte avec capacite restante via GET /personnes/:id (slug → resolution) - Embed
[projet:projet-alpha]→ render card avec status + heures realisees - Database view
[modules-a-attribuer]→ embed kanban filtré
Ces nodes appellent le bridge via fetch + cache cote client (5 min).
11.2 Routes pages full
Phase 2+ : le bridge sert aussi des pages full /personne/:id, /projet/:id, /formation/:id qui ressemblent a des pages Docmost (header layout + content).
Implementation : Hono cote backend rend HTML avec layout Docmost mimique + content custom. Le user clique sur une mention dans Docmost, ouvre la page bridge, voit le meme look.
Ou en Phase 3 : on contribue au repo Docmost upstream pour ajouter ces nodes nativement.
12. Sample request/response
Saisir heures realisees
PATCH /api/v1/attributions/42/heures-realisees HTTP/1.1
Host: bridge.acadenice.fr
Authorization: Bearer brg_xxxxx
Content-Type: application/json
{
"heures_realisees": 3.5,
"comment": "Cours JS du 2026-05-07 OK"
}
Response 200 :
{
"data": {
"attribution_id": 42,
"heures_attribuees": 10,
"heures_realisees": 3.5,
"statut": "en_cours",
"module": {
"module_id": 17,
"module_nom": "JS Fondamentaux",
"heures_realisees_total": 3.5
},
"personne": {
"personne_id": 5,
"nom_prenom": "Pierre Dupont",
"heures_attribuees_formation": 80,
"heures_restantes_formation": 670
}
}
}
Erreur RG violation
POST /api/v1/modules/17/attribuer HTTP/1.1
Authorization: Bearer brg_xxxxx
Content-Type: application/json
{
"personne_id": 5,
"heures_attribuees": 50,
"date_debut": "2026-09-01"
}
Response 422 :
{
"error": {
"code": "RG_VIOLATION",
"message": "Heures attribuees depassent la capacite du module",
"details": {
"rule": "RG-01",
"module_id": 17,
"heures_module": 30,
"heures_deja_attribuees": 0,
"heures_demandees": 50
}
}
}
13. Observabilite
13.1 Logs (Pino structured JSON)
Niveau info par defaut, debug en local. Format :
{
"level": "info",
"time": "2026-05-07T10:23:45.123Z",
"msg": "PATCH /api/v1/attributions/42/heures-realisees",
"method": "PATCH",
"path": "/api/v1/attributions/42/heures-realisees",
"status": 200,
"duration_ms": 142,
"user_token_id": "tok_abc123",
"request_id": "req_xyz789"
}
Champs sensibles redactes : pas de body en logs, pas de token en clair.
13.2 Metrics Prometheus
Exposees sur /api/metrics :
http_requests_total{method,path,status}counterhttp_request_duration_seconds{method,path}histogrambaserow_api_calls_total{endpoint,status}countercache_hits_total/cache_misses_totalwebhook_events_processed_total{type,outcome}
14. Tests
Cf doc 16 plan-de-tests :
- Unit Vitest 80% coverage minimum sur domain
- Integration tests avec testcontainers Baserow + Redis
- E2E playwright sur staging
15. Roadmap implementation
Phase 2.0 — Bootstrap (semaine 1-2)
- Setup Hono + zod + ofetch + pino
- BaserowClient avec tests integration
- DocmostClient skeleton
- Healthcheck endpoint
- Auth middleware basique (API token)
- CI/CD complet (cf doc 17)
- Deploy staging
Phase 2.1 — Read endpoints (semaine 3-4)
- GET /personnes/:id avec rollups calcules
- GET /projets/:id
- GET /formations/:id
- Cache Redis pattern cache-aside
- Tests integration sur les endpoints
Phase 2.2 — Write endpoints + webhooks (semaine 5-7)
- POST /interventions
- PATCH /attributions/:id/heures-realisees
- Webhooks Baserow handlers
- Validation RG-01 a RG-06
- Tests integration write
Phase 2.3 — Tiptap nodes (semaine 8-10)
- Premier node Tiptap custom (mention
@formateur) - Integration Docmost (fork ou plugin)
- E2E playwright
Phase 2.4 — Pages full + rapports (semaine 11-12)
- Routes /personne/:id, /projet/:id en page Docmost-style
- Endpoint /rapports/* PDF generation
- Stabilisation, fix bugs, doc utilisateur
16. Decisions a prendre
- Source of truth tokens : .env (simple) vs Postgres dedie (rotation a chaud) ? Mon vote : .env Phase 2, Postgres Phase 3
- Cache Redis partage Docmost ou dedie ? Partage Phase 2 (simple, sa marche), dedie Phase 3 si charge ou conflits
- PDF generation : Puppeteer (lourd) vs PDFKit (manuel) vs service externe (gotenberg) ? Recommande PDFKit ou gotenberg self-host
- OpenAPI 3 doc auto : generee depuis Zod schemas ? Lib
@asteasolutions/zod-to-openapi. A faire Phase 2.1. - GraphQL au lieu de REST ? Pas pertinent pour notre scope (peu de endpoints, peu de variation queries). REST est plus simple.
- Multi-tenant ? Pour l'instant non — Acadenice mono-instance. Si rachat / scaling : ajouter
tenant_idpartout. Pas avant Phase 4.
17. Glossaire
| Terme | Definition |
|---|---|
| Bridge | Service custom qui se sert d'intermediaire entre Docmost (UI) et Baserow (data) |
| Tiptap node-view | Composant React custom integre dans editeur Tiptap pour rendre un block specifique |
| Cache aside | Pattern : check cache → if miss, fetch source + populate cache |
| Idempotence | Une requete repetee a le meme effet qu'une requete unique (anti-doublon) |
| HMAC signature | Hash crypto pour verifier l'authenticite d'un payload (webhook) |
| Sliding window rate limit | Compteur sur fenetre glissante (ex: derniere minute) |
| RG | Regle de Gestion (Merise) |
| Scope (token) | Permission specifique (read:X, write:Y, admin:*) |