Aller au contenu principal

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.tsGET /me (profil Auth0 courant)JWT
tasks.tsPOST/GET/PATCH/DELETE /tasks[/:id]JWT
services.tsPOST/GET/PATCH/DELETE /services[/:id], PATCH /services/:id/activeJWT
services-public.tsGET /services/public, GET /services/public/by-slug/:slugpublic (PII filtrée)
users-public.tsGET /users/:id/services/public, GET /users/:id/profile/publicpublic
match.tsPOST /matchJWT
tracking.tsPOST/GET/PATCH /tracking[/:id]JWT
claim.ts · claim-redeem.tsPOST /services/:id/claim (token signé)token
refuse.tsPOST /refusetoken
reviews.tsGET /services/:id/reviews + résumésmixte
imports.tsPOST /imports/services (CSV), GET /imports/:idJWT
upload.tsPOST /uploads/url (URL S3 présignée)JWT
qstash.tsPOST /internal/qstash (worker async)signature QStash
admin.tsGET /admin/stats, GET /admin/users, GET /admin/users/:id, POST /admin/users/:id/blockJWT + admin
Pas de table users

Les 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

EndpointDescription
GET /admin/statsKPIs 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/:idProfil Auth0 complet + toutes les entités DB de cet utilisateur (tâches, services, profil, trackings, avis).
POST /admin/users/:id/blockSuspend (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 null

Le 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).