apps/api — backend Hono + Drizzle
Serveur Hono sur @hono/node-server (port 3001), lancé en dev via
tsx watch src/index.ts. Accès Postgres avec pg + drizzle-orm. Les SDK tiers
passent par les ports de packages/lib.
Arborescence
apps/api/src/
index.ts # bootstrap du serveur
app.ts # app Hono, CORS (WEB_URL), montage des routes
env.ts # getEnv() zod + parseAdminEmails() (cf. Installation)
container.ts # wiring des ports lib (cache, storage, llm, …)
db/ # schema.ts (Drizzle) + client
middleware/ # auth (vérif JWT), admin (liste email), etc.
routes/ # un fichier par domaine HTTP
match/ # pipeline de matching (classify, embed, pipeline, judge)
claim/ tracking/ imports/ workers/ # logique métier par domaine
scripts/ # backfills ponctuels
Référence des endpoints
Fichier (src/routes/) | Endpoints (principaux) | Auth |
|---|---|---|
me.ts | GET /me (profil Auth0 courant) | JWT |
tasks.ts | POST/GET/PATCH/DELETE /tasks[/:id] | JWT |
services.ts | POST/GET/PATCH/DELETE /services[/:id], PATCH /services/:id/active | JWT |
services-public.ts | GET /services/public, GET /services/public/by-slug/:slug | public (PII filtrée) |
users-public.ts | GET /users/:id/services/public, GET /users/:id/profile/public | public |
match.ts | POST /match | JWT |
tracking.ts | POST/GET/PATCH /tracking[/:id] | JWT |
claim.ts · claim-redeem.ts | POST /services/:id/claim (token signé) | token |
refuse.ts | POST /refuse | token |
reviews.ts | GET /services/:id/reviews + résumés | mixte |
imports.ts | POST /imports/services (CSV), GET /imports/:id | JWT |
upload.ts | POST /uploads/url (URL S3 présignée) | JWT |
qstash.ts | POST /internal/qstash (worker async) | signature QStash |
admin.ts | GET /admin/stats, GET /admin/users, GET /admin/users/:id, POST /admin/users/:id/block | JWT + admin |
usersLes utilisateurs vivent dans Auth0. L'API les identifie via c.var.user.sub
(le sub Auth0). Voir Authentification.
Slice admin
Les routes /admin/* sont réservées à la console Hermes. Elles
exigent un JWT valide et un email présent dans ADMIN_EMAILS (voir
Authentification → Middleware admin).
authMiddleware → adminMiddleware → handler admin
Endpoints
| Endpoint | Description |
|---|---|
GET /admin/stats | KPIs plateforme : utilisateurs Auth0 + agrégats DB (services, tâches, trackings par statut, avis). |
GET /admin/users?query=&page=&perPage= | Annuaire Auth0 paginé enrichi de compteurs DB (tâches, services, avis) et hasProfile. |
GET /admin/users/:id | Profil Auth0 complet + toutes les entités DB de cet utilisateur (tâches, services, profil, trackings, avis). |
POST /admin/users/:id/block | Suspend (blocked: true) ou réactive (blocked: false) un compte via l'API Management Auth0. |
Réponse /admin/stats
{
"registeredUsers": 142, // peut être null (voir note ci-dessous)
"services": { "total": 89, "active": 72, "inactive": 17 },
"tasks": { "total": 310 },
"trackings": { "contacted": 12, "doing": 8, "done": 47, "refused": 3, "total": 70 },
"reviews": { "total": 55 }
}
Chaque chiffre trace vers une vraie table Drizzle — pas de valeur inventée.
registeredUsers peut être nullLe compte d'utilisateurs Auth0 est récupéré en best-effort : si la
Management API échoue (quota, réseau), la réponse contient "registeredUsers": null et les KPIs DB restent disponibles. Hermes affiche "—" dans ce cas.
Réponse /admin/users (liste)
{
"items": [
{
"sub": "auth0|…", "email": "…", "givenName": "…", "familyName": "…",
"createdAt": "…", "blocked": false, "lastLogin": "…",
"counts": { "tasks": 3, "services": 1, "reviews": 2 },
"hasProfile": true
}
],
"total": 142, "page": 0, "perPage": 50
}
Le pipeline /match
POST /match accepte une requête en union discriminée :
type MatchQuery =
| { type: "task-to-service"; taskId: string } // services pour cette task
| { type: "service-to-task"; serviceId: string } // tasks pour ce service
| { type: "text"; input: string } // libre ; le juge tranche la direction
Invariant : limit ≤ topK. Le juge ne voit jamais plus de topK candidats
(c'est le bouton de coût, réglé par MATCH_JUDGE_TOPK).
Cycle de vie d'un tracking
Quand un client contacte un prestataire (POST /tracking), un enregistrement
relie la task au service et notifie l'owner. Le statut (tracking_status) avance :
En parallèle, notification_status suit l'acheminement de l'email :
queued → sent (ou no_contact / failed).
Cold start & claim
La supply est amorcée par scrape Google Maps. Les services scrapés vivent
non-revendiqués (userId IS NULL, source = "google_scrape") et notifient
le contact scrapé via un email contenant un claimToken signé (HS256,
CLAIM_SECRET).