Application mobile de parcours urbains clé en main. Paris d'abord.
- Spécification : le wiki est le cahier des charges.
- Tickets : les issues, organisées par milestones
v0.1àv1.0. - Méthode : CLAUDE.md (modes de travail, consignes de sécurité) ; SECURITY.md pour signaler une vulnérabilité.
apps/mobile Expo + expo-router
apps/api Fastify + Drizzle + PostGIS
apps/admin React + Vite (modération)
packages/shared schémas Zod, types, taxonomies, libellés
packages/orchestration moteur de cohérence des parcours
packages/api-client client typé
infra déploiement GCP
Prérequis : Node 22 (.nvmrc), Docker. pnpm est fourni par corepack à la version fixée dans package.json.
nvm use && corepack enable
pnpm install
docker compose up -d # Postgres 16 + PostGIS sur localhost:5432
cp apps/api/.env.example apps/api/.env
pnpm --filter api db:migrate # schéma de la base widoo, celle de pnpm dev
pnpm lint && pnpm typecheck && pnpm test # l'API teste sur widoo_test, créée et migrée au lancement
pnpm buildPort 5432 déjà pris : copier .env.example en .env à la racine, changer POSTGRES_PORT et reporter le port dans apps/api/.env. Vérifier PostGIS : docker compose exec db psql -U widoo -c 'select postgis_version()'.
| Commande | Effet |
|---|---|
pnpm lint |
ESLint dans chaque workspace, puis Prettier en vérification |
pnpm typecheck |
TypeScript strict, sans émission |
pnpm test |
Vitest (pnpm test -- --coverage pour la couverture, comme en CI) |
pnpm build |
build des workspaces qui en ont un |
pnpm format |
Prettier en écriture |
pnpm dev |
serveurs de développement (API et admin, à mesure qu'ils arrivent) |
La configuration TypeScript, ESLint, Prettier et Vitest vit à la racine et chaque workspace en hérite.
Langues : code et commits en anglais ; wiki, issues et interface en français.
Tout changement passe par une PR remplie selon .github/pull_request_template.md (règles : CLAUDE.md, mode CODE). Le ruleset de main exige une PR et ces trois checks, et y interdit suppression et force-push :
| Check | Workflow | Vérifie |
|---|---|---|
Lint, types, tests |
ci.yml |
install figé, lint, types, migration de la base de test (PostGIS), tests avec couverture, build API et admin, pnpm audit (élevé et critique). Vert sans rien lancer si la PR ne touche que *.md, docs/, .claude/, .github/ ou scripts/*.sh |
Title, body, issue link |
pr-policy.yml |
titre <type>(<scope>): <action> de 72 caractères au plus ; sections du modèle, Fixes #N (ou Refs #N), Niveau : et Formatage mécanique :, hors commentaires HTML |
Diff size |
pr-policy.yml |
lignes utiles de chaque commit de la PR, hors merges, lockfile, snapshots, fichiers générés, SVG et PNG, puis le total Useful diff: N lines, sans plafond ; refus si un commit dépasse 500 lignes, ou 400 sans ligne Taille : renseignée. Script : .github/scripts/diff-size.sh <base> <head> |
Exception d'audit (#279). pnpm audit --audit-level=high bloque toute alerte élevée ou critique, sauf les identifiants listés dans auditConfig.ignoreGhsas de pnpm-workspace.yaml : deux alertes sans version corrigée en amont, node-forge (GHSA-86w9-cpqp-85rv) et braces (GHSA-vfj7-8cjw-p6xm), qui n'atteignent ni l'app livrée ni l'image de l'API. Risque accepté le 2026-10-05 (Securite-et-RGPD › Risques acceptés), à revoir le 2026-11-05 (#280) ou dès qu'une version corrigée sort. pnpm ne sait ni dater un identifiant ni le limiter à un chemin de dépendance : la révision se fait à la main et revérifie les chemins. Pour retirer une alerte : supprimer son identifiant du bloc (et le bloc s'il est vide), monter la dépendance si un correctif existe, relancer pnpm audit --audit-level=high, qui doit sortir en 0 avec un « ignored » de moins. L'étape CI suivante, scripts/check-audit-exceptions.sh, rejoue l'audit du lockfile sans l'exception et échoue dès qu'un identifiant ignoré ne correspond plus à aucune alerte (dépendance montée, alerte retirée) : une exception devenue inutile ne survit pas à la PR qui la rend inutile. Elle n'échoue jamais sur une date. Jamais de --ignore-unfixable : il ignorerait aussi toute future alerte sans correctif.
ci.yml tourne à chaque push. pr-policy.yml tourne aussi quand le titre, le corps ou la base de la PR changent : corriger le corps suffit à relancer ses checks. « Re-run » sur un ancien run relit le corps de l'époque, pas le nouveau.
Plafond par commit (CLAUDE.md › CODE). La PR est fusionnée en rebase and merge : chaque commit arrive tel quel sur main, et le plafond porte sur chaque commit. Viser moins de 400 lignes utiles par commit ; jusqu'à 500 si la ligne Taille : du corps cite le commit, par son titre (son SHA change au rebase), et justifie sa taille ; au-delà, redécouper le commit. La PR affiche son total, sans plafond. Seuls comptent les commits propres à la PR (base..head), et le total part du point de divergence avec la base (base...head) : les commits arrivés sur la base depuis la création de la branche ne sont pas comptés.
PR empilées. Une PR par ticket : on ne découpe pas un ticket en plusieurs PR, et une pile ne relie que des tickets dépendants. Seule main est protégée : une PR dont la base est une branche issue/... affiche ses checks sans les exiger, et Fixes #N ne ferme le ticket qu'à la fusion dans main. On ne fusionne donc jamais une PR dans sa base de pile. Ilan ou Paul fusionne la PR du bas de la pile dans main en rebase and merge. Sur la branche suivante, on rebase sur main : git saute les commits déjà fusionnés à l'identique. Ilan ou Paul pousse la branche rebasée, puis on recible la PR et on attend ses trois checks verts avant de la fusionner à son tour :
git fetch origin && git rebase origin/main # sur la branche suivante
git push --force-with-lease # Ilan ou Paul
gh pr edit <n° de la PR suivante> --base mainVariables d'environnement décrites et validées au démarrage : apps/api/.env.example.
Erreurs 500 : journalisées puis envoyées à Sentry (src/monitoring.ts) si SENTRY_DSN est posé, après nettoyage par loggableError et le filtrage partagé (packages/shared/src/monitoring.ts). L'utilisateur y est réduit à users.id ; aucun en-tête Authorization ni cookie, aucune query string.
pnpm --filter api dev # http://localhost:8080/v1/health, OpenAPI sur /docs (hors production)
pnpm --filter api build && pnpm --filter api start
docker build -f infra/api.Dockerfile -t widoo-api .
docker run --rm -p 8080:8080 -e DATABASE_URL=postgres://widoo:widoo@host.docker.internal:5432/widoo widoo-apiCloud Run tourne en linux/amd64 : ajouter --platform linux/amd64 au build sur Apple Silicon.
Schéma unique : apps/api/src/db/schema.ts (Drizzle ORM). Migrations versionnées sous apps/api/src/db/generated/migrations, écrites par db:generate et relues avant commit.
Commande (pnpm --filter api …) |
Effet |
|---|---|
db:generate --name=add_x |
écrit la migration du dernier changement de schema.ts ; --custom --name=x crée un fichier SQL à écrire à la main |
db:migrate |
applique les migrations en attente, sans effet si la base est à jour (la CI migre la base de test avant les tests) |
db:seed |
jeu de démonstration (apps/api/src/db/seed/demo-routes.json) : Paris, 10 parcours sur des lieux réels, auteurs fictifs, photos Unsplash ; idempotent (une deuxième exécution ne change rien, un JSON modifié est appliqué), refusé en production |
db:reset |
vide la base locale (tables, types, extensions, journal des migrations), puis db:migrate et db:seed ; refusé hors localhost et en production |
node dist/demo-seed.js load|remove |
dans l'image, staging seulement : charge ou retire le jeu de démonstration ; exige DEMO_SEED=staging et refuse hors du projet GCP widoo-staging, production comprise. Lancé par infra/gcp/demo-seed.sh (infra/README.md) |
Base de test. Les tests de l'API n'écrivent jamais dans widoo, la base de pnpm dev : ils tournent sur widoo_test, sur le même serveur. Avant les tests, le setup global de Vitest (apps/api/src/test-global-setup.ts) la crée si elle manque, puis applique les migrations en attente ; aucune commande à lancer. DATABASE_URL, s'il est posé dans le shell, la remplace, mais son nom doit finir par _test, sinon les tests refusent de démarrer : c'est le cas de la CI, dont la base éphémère s'appelle widoo_test. Port 5432 changé : DATABASE_URL=postgres://widoo:widoo@localhost:<port>/widoo_test pnpm test. Pour repartir d'une base de test vide (après une migration abandonnée sur une autre branche) : docker compose exec db dropdb -U widoo widoo_test, recréée au prochain pnpm test.
Dans l'image, docker run --rm -e DATABASE_URL=… widoo-api node dist/migrate.js applique les migrations avant un déploiement.
Expo SDK 57, expo-router, apps/mobile. L'app appelle l'API locale : la démarrer avec HOST=0.0.0.0 pnpm --filter api dev pour qu'un simulateur, un émulateur ou un téléphone du réseau la joigne.
pnpm --filter mobile ios # build de développement (Xcode), installé sur le simulateur, puis Metro
pnpm --filter mobile android # idem sur l'émulateur (Android SDK, JDK 17)
pnpm --filter mobile start # Metro seul, pour un build de développement déjà installéExpo Go ne suffit plus : MMKV (consentement) est un module natif absent d'Expo Go. Après l'ajout d'un module natif, relancer ios / android pour reconstruire.
En développement, l'URL de l'API est l'adresse de la machine qui sert Metro, port 8080 (src/api/api-url.ts) ; EXPO_PUBLIC_API_URL dans apps/mobile/.env.local la remplace. Un build EAS lit EXPO_PUBLIC_API_URL dans les variables de son environnement EAS (eas.json). Les dossiers ios/ et android/ sont générés (expo prebuild) et jamais versionnés.
Analytics : PostHog Cloud UE, seulement après « Accepter » sur la bannière de consentement (src/analytics). Seuls les builds preview et production (EXPO_PUBLIC_APP_ENV, posé par eas.json) envoient des événements, chacun dans son projet : « Widoo - Staging » et le projet de production, à créer (#264), avec la clé EXPO_PUBLIC_POSTHOG_KEY de l'environnement EAS du profil. En développement local, aucune clé PostHog : EXPO_PUBLIC_POSTHOG_KEY reste vide dans .env.local, et l'app ignore une clé présente, donc rien n'est envoyé ; la bannière s'affiche quand même. Pour la revoir, désinstaller l'app. Un événement se vérifie d'abord dans le journal local : EXPO_PUBLIC_ANALYTICS_DEBUG=1 écrit chaque événement accepté dans le journal de Metro ([analytics] <événement> <propriétés>, nom et propriétés), seulement après consentement ; un événement hors du catalogue de packages/shared est refusé et signalé en erreur. Son arrivée réelle se vérifie ensuite sur un build preview, dans le projet « Widoo - Staging ».
Rapports de plantage : Sentry, avec ou sans consentement mais sans donnée utilisateur (src/monitoring, filtrage dans packages/shared/src/monitoring.ts). Sans EXPO_PUBLIC_SENTRY_DSN, rien n'est envoyé. L'envoi des source maps et symboles est coupé (SENTRY_DISABLE_AUTO_UPLOAD=true dans les scripts ios / android et dans eas.json) tant qu'aucun jeton Sentry n'est posé en secret EAS.
Firebase Authentication (E-10) passe par les SDK natifs : @react-native-firebase/app et auth, plus @react-native-google-signin/google-signin pour le bouton Google. La session est gardée par Firebase sur le téléphone. Le jeton ne sort de src/auth que dans l'en-tête Authorization des requêtes à l'API.
Les fichiers Firebase ne sont jamais versionnés. Leurs clés sont publiques mais restreintes à app.widoo, et chaque environnement apporte son projet. app.config.ts les lit dans cet ordre :
- Sur EAS, les variables de type fichier
GOOGLE_SERVICES_JSONetGOOGLE_SERVICE_INFO_PLISTde l'environnement du profil. Un build EAS sans elles échoue tout de suite, au lieu de planter au démarrage. - En local, le dossier
apps/mobile/firebase/, ignoré par git, avec les deux fichiers sous leur nom d'origine :google-services.jsonetGoogleService-Info.plist. Ils se téléchargent depuis la console Firebase (projetwidoo-staging, Paramètres du projet, Vos applications, Android puis iOSapp.widoo). Sans eux, le build de développement se construit, mais la connexion n'y marche pas.
Après la pose ou le changement de ces fichiers, reconstruire l'app (pnpm --filter mobile ios / android) : ils sont lus au prebuild, pas par Metro. En local, EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID se met dans apps/mobile/.env.local.
Lien de connexion par e-mail : Firebase l'envoie depuis le domaine <projet>.firebaseapp.com, autorisé par défaut, sur le chemin /__/auth/links. L'app s'y déclare : App Links sur Android (autoVerify), domaines associés sur iOS (applinks:). Le domaine se déduit du projet de google-services.json. Pour que le lien ouvre l'app plutôt que le navigateur, la console Firebase doit connaître l'empreinte SHA-256 du certificat qui signe le build (Android) et le Team ID Apple (iOS) : Firebase Hosting en tire assetlinks.json et apple-app-site-association. Ouvert dans l'app, le lien passe par app/+native-intent.tsx, qui le donne au parcours de connexion sans changer d'écran : la sheet E-10 s'ouvre sur l'écran en cours.
L'app est reliée au projet EAS @inprogress-agency-team/widoo, propriété de l'organisation Expo inprogress-agency-team, par owner et extra.eas.projectId dans apps/mobile/app.json. Ces deux valeurs sont publiques et versionnées ; src/eas-project.test.ts vérifie leur présence et que chaque profil lit l'environnement EAS de son nom. Les commandes eas qui écrivent (init, env:create, build, credentials) reviennent à Ilan ou Paul, et se lancent toujours depuis apps/mobile : à la racine, eas init crée un app.json parasite. eas init réécrit aussi app.json (permissions Android de localisation, extra.router vide, formatage) : n'en garder que owner et extra.eas.projectId, le reste est ajouté par les plugins expo-location et expo-router quand la configuration est évaluée.
Profils de apps/mobile/eas.json : development (client de développement, distribution interne), preview (distribution interne, sur l'API de staging) et production (stores, numéro de build incrémenté par EAS, appVersionSource: remote). Chacun lit les variables de l'environnement EAS du même nom (champ environment) et pose lui-même EXPO_PUBLIC_APP_ENV et SENTRY_DISABLE_AUTO_UPLOAD.
Variables par environnement EAS. Les noms seulement : aucune valeur dans le dépôt, les tickets ou les rapports.
| Variable | development |
preview |
production |
Rôle |
|---|---|---|---|---|
EXPO_PUBLIC_API_URL |
vide | API de staging (Cloud Run) | à poser : API de production | HTTPS obligatoire hors développement ; absente, un build preview ou production plante au démarrage |
EXPO_PUBLIC_MAPBOX_TOKEN |
vide | jeton public widoo-mobile |
à poser | jeton public (pk.) ; absent, un build preview ou production plante au démarrage |
EXPO_PUBLIC_POSTHOG_KEY |
vide (D-059) | projet « Widoo - Staging » | à poser quand le projet de production existera (#264) | ignorée hors preview et production, même présente |
EXPO_PUBLIC_SENTRY_DSN |
vide | posée | à poser | absente : aucun rapport de plantage |
EXPO_PUBLIC_APP_ENV |
development |
preview |
production |
posée par eas.json, jamais dans EAS |
GOOGLE_SERVICES_JSON |
fichier du projet widoo-staging |
fichier du projet widoo-staging |
à poser : projet de production | variable de type fichier, « Sensitive » ; google-services.json de l'app Android app.widoo (voir Connexion) |
GOOGLE_SERVICE_INFO_PLIST |
fichier du projet widoo-staging |
fichier du projet widoo-staging |
à poser | variable de type fichier, « Sensitive » ; GoogleService-Info.plist de l'app iOS app.widoo |
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID |
posée | posée | à poser | client OAuth « Web » du projet Firebase, public ; sans lui, Google ne rend pas de jeton sur Android |
EXPO_PUBLIC_ANALYTICS_DEBUG |
— | — | — | apps/mobile/.env.local seulement, lue en développement |
development ne porte que ce que lit le build natif (les fichiers Firebase) et le client Google, exprès : un client de développement reçoit son JavaScript du Metro local, qui lit apps/mobile/.env.local ; l'URL de l'API y est par défaut celle de la machine, et aucune clé d'analytics n'existe en développement (D-059). Une variable EXPO_PUBLIC_* est embarquée dans l'app et lisible par quiconque l'installe : elle se crée en visibilité « Plain text », jamais « Secret » ni « Sensitive », qui ne la protégeraient pas. Une vraie clé secrète (jeton Sentry d'envoi des symboles, par exemple) n'a jamais le préfixe EXPO_PUBLIC_ et se crée en « Secret ».
RNMAPBOX_MAPS_DOWNLOAD_TOKEN n'est plus utile : Mapbox ne demande plus de jeton pour télécharger son SDK, et le plugin de @rnmapbox/maps 10.3 ne s'en sert que s'il est posé (dépôt Maven sans authentification sinon). Il ne figure plus dans .env.example ni dans aucun environnement EAS.
expo-updates n'est pas installé (son installation est un ticket à part de la Roadmap) : les champs channel de preview (staging) et production (production) sont sans effet sur l'app, et EAS n'y crée aucun canal. eas build affiche alors un avertissement, puis propose d'installer expo-updates et de configurer EAS Update : répondre non.
Build preview, par Ilan ou Paul, depuis apps/mobile :
cd apps/mobile
eas build --profile preview --platform androidLe premier build Android demande de générer le keystore, gardé par EAS (credentialsSource: remote). Le build .apk s'installe par le lien ou le QR code qu'EAS affiche. iOS attend le compte Apple Developer de l'agence : aucun build iOS installable pour l'instant. Tant que le jeu de démonstration n'est pas chargé dans la base de staging, un build preview montre une carte vide : infra/gcp/demo-seed.sh load l'y charge (infra/README.md).