Wiki/docs/19-bridge-api-design.md
Corentin JOGUET 668576cdc4 chore: initial commit — formation-hub conception phase
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)
2026-05-07 12:16:19 +02:00

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 :

  1. Exposer les rows Baserow comme objets typed au reste de l'ecosysteme (typing strict, validation, cache)
  2. Recevoir les webhooks Baserow pour invalider caches et declencher actions (notifications, recalculs cross-zone)
  3. Servir les Tiptap node-views custom dans Docmost (mention @formateur, embed [projet], etc.) avec donnees fraiches
  4. 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} counter
  • http_request_duration_seconds{method,path} histogram
  • baserow_api_calls_total{endpoint,status} counter
  • cache_hits_total / cache_misses_total
  • webhook_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_id partout. 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:*)