Pilote Orléans · configuration, pas un fork · aucun lieu inventé

Livrable 11

API

Style

  • Préfixe /api/v1.
  • REST. JSON. Erreurs typées { "error": { "code", "message" } } — message sûr pour l'utilisateur, détail en log serveur.
  • Pagination curseur { "data", "next_cursor" } sur les listes.
  • Auth : session cookie httpOnly, Secure, SameSite (Better Auth) quand le compte existe. Les routes publiques ne demandent pas de session.
  • Autorisation dans le handler, jamais seulement dans le routeur UI.
  • Validation Zod sur chaque body et query.
  • OpenAPI généré depuis ces schémas (livrable d'implémentation, pas de cette phase). Rate limit : voir déploiement (mémoire en un seul process dev ; Redis ou limite edge en prod — THIRD-PARTY si edge).

Surfaces

MéthodeCheminAccèsRôle
GET/citiespublicvilles published seulement. draft et soft_launch absents.
GET/cities/:slugpublic si published ; staff si soft_launch ; sinon 404DNA publiée + ancrages. soft_launch : noindex.
GET/cities/:slug/navigation?at=publicnav résolue à un instant
GET/cities/:slug/zonespubliczones publiées
GET/cities/:slug/now?zone=&bbox=publicNow engine
GET/cities/:slug/tonightpublicfenêtre locale « ce soir »
GET/cities/:slug/weekendpublicfenêtre week-end
GET/cities/:slug/search?q=publicrecherche ; noindex côté HTML
GET/cities/:slug/venues/:venueSlugpublicfiche + bloc live
GET/cities/:slug/events/:eventSlugpublicfiche
GET/cities/:slug/map?bbox=&filters=publicobjets viewport, plafonnés
POST/planspublic ou usercréer un plan P1 ; anonyme permis, éphémère
GET/pro/todaymembercompteurs réels
POST/pro/live-updatesmember APPROVEDpublier
POST/pro/eventsmemberstudio
POST/pro/offersmemberoffre
POST/pro/claimsuserdemande
GET/city/:cityId/qualitycity staffcomptes qualité
PUT/city/:cityId/dnacity managerbrouillon
POST/city/:cityId/dna/publishcity managerpublier
POST/city/:cityId/dna/rollbackcity managerrevenir à une version stockée
POST/admin/citiessuperwizard
POST/reportspublic limitésignalement

Pas de /admin accessible par un rôle city. Pas d'ID d'une autre ville accepté « parce que le client l'envoie ».

Now

GET /now est aussi disponible en alias sous la ville. Réponse :

{
  "evaluated_at": "ISO-8601",
  "timezone": "Europe/Paris",
  "definition": "open_now | event_active | event_starting_soon | offer_active, non expirés",
  "results": []
}

Chaque item : id, type, title, status_temporel, distance_m ou distance_unknown, provenance, reasons[] courtes. Jamais de fréquentation.

event_starting_soon : time_windows.starting_soon_minutes de la DNA (défaut proposé 90, non validé). Pas une seconde constante dans le code.

Chaque item porte source_type, verification_status, temporal_status et provenance (phrase). Pas un badge unique. distance_m seulement avec une origine ; sinon distance_unknown: true. Filtre distance absent sans origine.

Search

Query : q, zone, when (now|tonight|weekend|range), category, open_now, bbox, limit.

Réponse : results, interpreted_intent (peut être nul si non parsé), gaps non exposé au public.

Erreurs

codequand
validation_errorpayload
not_foundslug inconnu ou autre ville
forbiddenrôle
unverified_claimpublish sans claim approuvé
rate_limitedabus
dependency_unavailabletuile, routeur, IA — la ressource principale reste servie si possible
legal_holdobjet alcool public alors que flag off

Décisions

D1 — REST unique, pas de BFF par produit

  • Choix : une API, trois clients (pages) dans la même app.
  • Alternatives : GraphQL ; trois API.
  • Compromis : over-fetch évité par des DTO explicites (VenueCard, VenueDetail), pas par un graphe libre.
  • Multi-ville : le slug de ville est dans le chemin public. L'admin utilise l'UUID.

D2 — Écritures pro idempotentes

Idempotency-Key sur publish, claim, campagne, webhook Stripe. Double tap ≠ double happy hour.

D3 — Contrats avant UI large

Les DTO de Now, Search, VenueCard, LiveUpdate sont spécifiés avant les écrans. L'UI ne devine pas un champ.

Flags

  • OpenAPI : à générer à l'implémentation, pas un fichier fictif « déjà publié ».
  • Webhooks Stripe : THIRD-PARTY, signature obligatoire, hors MVP fonctionnel.